Skip to content

fix(rest): request bodies — correct what was silently wrong, add what was missing, refuse what cannot be written - #193

Merged
ako merged 4 commits into
mainfrom
claude/bootstrap-prompt-smaller-37u3fu
Aug 20, 2026
Merged

fix(rest): request bodies — correct what was silently wrong, add what was missing, refuse what cannot be written#193
ako merged 4 commits into
mainfrom
claude/bootstrap-prompt-smaller-37u3fu

Conversation

@ako

@ako ako commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Three commits on REST request bodies, each measured against Studio Pro-authored microflows in ako/TestApp (Mendix 11.13.0) covering Custom, Mapping, FormData and Binary.

Every defect here shares a shape: the model stays valid, so nothing reports the loss. mxcli check passes, mx check reports 0 errors, the app builds and the request often returns 200 — and the payload is wrong or absent.

1. cbed3b0body: file sent the literal text $Doc

On a consumed REST operation, Body: file from $Doc was written as a Rest$StringBody holding the expression text. Measured against httpbingo with an 8090-byte PNG:

Content-Length: 4
data:application/octet-stream;base64,JERvYw==   ->  b'$Doc'

Both engines folded FILE into the TEMPLATE branch — consumed_rest_write.go (default) and writer_rest.go (legacy). describe rendered it back as Body: template '$Doc', so the round trip looked self-consistent.

There is no correct type to write: the metamodel has exactly three request-body types (Rest$JsonBody, Rest$StringBody, Rest$ImplicitMappingBody), none binary. So the clause is refused as MDL-REST02, the way MDL-REST01 refuses a mapping document inline. One function guards both the check pass and exec, and it sits in buildRestClientOperation, so --no-check does not reopen the silent write.

The refusal caught two shipped examples advertising a "File Upload" feature that never worked; 06-rest-client-examples.mdl is corrected.

2. 79c473f — binary POST, which is expressible

The commit above concluded binary upload was impossible and pointed at a Java action. That was the right answer about the wrong document. Mendix models a binary body on the microflow REST CALL activity:

"RequestHandling":     {"$Type": "Microflows$BinaryRequestHandling",
                        "Expression": "$FirstFileDoc/Contents"},
"RequestHandlingType": "Binary"

It is a Microflows$ type — which is why grepping the metamodel for Rest$*Body finds only the three non-binary ones and appears to prove it impossible. A Studio Pro example settled it in minutes.

mxcli could parse that shape and could neither write, read on the modelsdk engine, nor describe it. So a Studio Pro binary POST described as a REST call with no body at all, and re-executing that DESCRIBE produced a request that sent nothing.

Wired full-stack: grammar (BODY BINARY expression), AST, visitor, builder, both writers, the modelsdk reader, the formatter. The expression is the FileDocument's Contents member and is carried as source text — quoting it would send the path as a string literal. MDL-REST02's message, the syntax help and the examples now point here instead of at a Java action.

3. 733d6db — the export-mapping body, and a guard

generated/metamodel gives MicroflowsMappingRequestHandling exactly three properties: contentType (Json|Xml), mappingId, mappingVariableName.

Property Studio Pro mxcli before
RequestHandlingType Mapping Custom
variable key MappingVariableName ParameterVariable
ContentType Json ''

So mxcli wrote a key the type does not own and omitted the real one — and an unknown property is the shape mxbuild tolerates and Studio Pro refuses to open. mxcli's own reader had known the correct key since mendixlabs#843; the writer was never corrected.

RequestHandlingType was hardcoded "Custom" in both engines regardless of handler; it is now derived. (The previous commit derived only Binary, the one case with a reference at the time — four examples made the rule measurable rather than guessed.)

Finally, FormDataRequestHandling and AdvancedRequestHandling can be parsed and not written, so a rewrite dropped the body silently. That is now refused (guard-don't-drop, ADR-0005), mirroring the queued-call guard. Its allow-list is the writable set, so it stops refusing the moment a type becomes expressible — as Binary just did.

Testing

Each fix was written test-first and fails on revert. Verified by round-tripping Studio Pro's own microflows and diffing the BSON — mxcli now reproduces the binary and export-mapping actions exactly (same $Type, discriminator and properties), and the form-data microflow is refused by name instead of silently emptied.

go test ./... green, make check-mdl 354 PASS / 0 FAIL, gofmt clean — re-run after rebasing onto current main.

New fixtures: rest-file-request-body.fail.mdl, rest-binary-post.mdl. Three symptom rows appended to fix-issue.md. Syntax help and MDL_QUICK_REFERENCE.md updated.

Also adds cmd/bsondump, a dev helper printing a .mxunit as canonical extended JSON — the technique the symptom rows prescribe, since bson.M loses key order and non-canonical extJSON hides int32 vs int64.

Not fixed here

  • The legacy engine encodes MappingId as binary where Studio Pro stores a qualified-name string. Changing an ID encoding without a test showing that path ever worked is the kind of blind change these bugs came from. Noted in the symptom table.
  • Authoring form-data bodies (only the refusal is here).
  • Four Studio Pro divergences found while chasing the import-range bug and proven not causal: int32 vs int64 on 14 mapping numerics, root MinOccurs 0 vs 1, missing MessageDefinition2, blanked OriginalValue — each real and still unfixed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg


Generated by Claude Code

claude added 4 commits August 19, 2026 21:35
…EST02)

`Body: file from $Doc` on a consumed REST operation sent the four bytes `$Doc`
instead of the document. Measured against httpbingo with an 8090-byte PNG:

    Content-Length: 4
    data:application/octet-stream;base64,JERvYw==   ->  b'$Doc'

mxcli check passed, mx check reported 0 errors and the call returned HTTP 200.
Every signal a user or an agent checks said success.

Both engines folded FILE into the TEMPLATE branch and wrote a Rest$StringBody
whose ValueTemplate is the expression text — consumed_rest_write.go:218 (the
default modelsdk engine) and writer_rest.go:250 (legacy). `describe` renders it
back as `Body: template '$Doc'`, so the round trip looked self-consistent.

There is no better type to write. Mendix's 11.13 metamodel has exactly three
request-body types — Rest$JsonBody, Rest$StringBody, Rest$ImplicitMappingBody —
and none is binary, so binary upload is not expressible in MDL at all. The fix
is therefore to refuse the clause, as MDL-REST01 refuses a mapping document in
an inline mapping, rather than degrade it into something that looks like it
works. A Java action is the route.

One function guards both the check pass and exec, so `mxcli check` and
`mxcli exec` cannot disagree — and because it sits in buildRestClientOperation,
`--no-check` does not reopen the silent write either.

The refusal is narrow: `Response: file as $Doc` downloads correctly and is
untouched.

Two shipped examples advertised this as a working "File Upload" feature and
never worked; 06-rest-client-examples.mdl is corrected — one is rewritten as
the download that does work, the other replaced by a note.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg
…nts`)

Binary POST was reported as impossible in MDL — a consumed REST operation has
only Rest$JsonBody, Rest$StringBody and Rest$ImplicitMappingBody, none binary,
so the previous commit refused `Body: file` and pointed at a Java action.

That was the right conclusion about the wrong document. Mendix models a binary
request body on the microflow REST CALL activity:

    "RequestHandling":     {"$Type": "Microflows$BinaryRequestHandling",
                            "Expression": "$FirstFileDoc/Contents"},
    "RequestHandlingType": "Binary"

It is a Microflows$ type, which is why grepping the metamodel for Rest$*Body
finds only the three non-binary ones and appears to prove it impossible. A
Studio Pro-authored example (ako/TestApp, 11.13.0) settled it.

mxcli could PARSE that shape and could neither write, read on the modelsdk
engine, nor describe it. So a Studio Pro binary POST described as a REST call
with no body at all, and re-executing that DESCRIBE produced a request that
sent nothing.

Wired full-stack: grammar (`BODY BINARY expression`), AST, visitor, builder,
both writers, the modelsdk reader and the DESCRIBE formatter. The expression is
the FileDocument's Contents MEMBER and is carried as source text — quoting it
would send the path as a string literal.

RequestHandlingType was hardcoded "Custom" in both engines regardless of the
handler. Only the Binary case is derived; the others are left alone, having no
measured Studio Pro reference and working today.

Verified by re-executing mxcli's DESCRIBE of the Studio Pro microflow and
diffing the BSON: same $Type, same discriminator, same expression, mxbuild 0
errors. `go test ./...` green, `make check-mdl` 354 PASS / 0 FAIL.

MDL-REST02's message, the syntax help, the shipped examples and the symptom
table now point at this route instead of a Java action.

Adds cmd/bsondump, a dev helper that prints a .mxunit as canonical extended
JSON — the technique the symptom table prescribes for this class of bug (plain
bson.M loses key order and hides int32 vs int64).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg
… write

Four defects in the REST call's request handling, all measured against Studio
Pro microflows (ako/TestApp, Mendix 11.13.0) covering Custom, Mapping, FormData
and Binary.

1. The export-mapping body wrote "ParameterVariable". generated/metamodel gives
   MicroflowsMappingRequestHandling exactly three properties — contentType,
   mappingId, mappingVariableName — so mxcli wrote a key the type does not own
   AND omitted the real one. An unknown property is the shape mxbuild tolerates
   and Studio Pro refuses to open. mxcli's own READER had known the correct key
   since mendixlabs#843; the writer was never corrected.

2. ContentType was written empty, which is not a member of the enum (Json|Xml).
   Studio Pro writes "Json".

3. RequestHandlingType was hardcoded "Custom" in both engines regardless of the
   handler, so an export-mapping body claimed to be a custom template. Studio
   Pro pairs Mapping/FormData/Binary/Custom with the matching sub-element; it is
   now derived. (The previous commit derived only Binary, the one case with a
   reference at the time.)

4. FormDataRequestHandling and AdvancedRequestHandling can be parsed and not
   written, so CREATE OR REPLACE/MODIFY dropped the body. Nothing reported it:
   DESCRIBE omits a clause it cannot express, and a REST call with no body
   builds clean, so the app posted nothing and every signal said fine. The
   rewrite is now refused (guard-don't-drop, ADR-0005), mirroring the queued-call
   guard. The allow-list is the writable set, so the guard stops refusing as
   soon as a type becomes expressible — as Binary just did.

Verified by round-tripping Studio Pro's own microflows: DESCRIBE → exec now
reproduces the export-mapping action exactly (type=Mapping, ContentType=Json,
MappingVariableName=NewItem), and the form-data microflow is refused by name
instead of silently emptied.

`go test ./...` green, `make check-mdl` 354 PASS / 0 FAIL.

Not fixed here: the legacy engine encodes MappingId as binary where Studio Pro
stores a qualified-name string. Noted in the symptom table.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg
The code changes landed with syntax help, the quick reference and the examples
updated, but not the two SKILLS that `mxcli init` syncs into user projects, nor
the docs site. Those are where an agent or a developer actually looks:

- rest-client.md has a "Body Types" section listing json/template/mapping. It
  now says there is no binary body on a consumed operation, that
  `body: file from $Doc` is refused as MDL-REST02, and where binary POST does
  live. `response: file as $Doc` is called out as unaffected.
- write-microflows.md's REST CALL section gains a binary POST example next to
  the JSON-body one.
- docs-site gains a "POST a Binary Body (File Upload)" section.

The docs-site example is a complete microflow, so it was validated the way the
rest of this work was rather than by eye — and the first version was WRONG:
`RETURNS response` binds a System.HttpResponse, and returning it from a
`RETURNS String` microflow is CE0117 at the end event. `mxcli check` passed it;
mxbuild caught it. The committed version returns Boolean from the status code
and is mxbuild-clean, and the page now states the constraint.

`mdl-examples/bug-tests/rest-binary-post.mdl` was validated the same way (0
errors), since check-mdl only runs `mxcli check` on fixtures.

make check-skill-mdl: 217 blocks checked, all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg
@ako
ako merged commit 603aeb4 into main Aug 20, 2026
9 checks passed
ako pushed a commit that referenced this pull request Aug 20, 2026
Two failures in one session came from trusting local state instead of checking
it, and both were reported to the user as fact before being caught.

1. The container is ephemeral and is re-cloned when reprovisioned, so commits
   made earlier in a session can be absent from the working copy while still
   present on the remote. It happened twice. The second time it was misread as a
   code bug: a grammar rule was "missing" from the tree, and several turns went
   into bisecting a parse failure in a binary built from the rolled-back tree.
   The rule had been committed and pushed hours earlier.

2. Commits were pushed onto a branch whose pull request had already merged, and
   described as "added to PR #N" when #N was closed and contained none of them.
   A merged PR cannot take new commits.

SessionStart now reports, and stays silent otherwise:
  - HEAD behind origin/<branch> — the stale-checkout signal
  - bin/mxcli built from a different commit than HEAD (the Makefile already
    stamps it via -X main.Version), so behaviour observed through the binary is
    known to come from the code under test

PostToolUse on Bash runs after any command containing "git push" and reports a
branch that is behind origin/main, which is the state both PR failures shared —
main had absorbed the branch's earlier commits via the merge. It distinguishes
"behind with commits of its own" (a merged PR cannot carry them) from "behind
with none" (stale checkout or merged PR) and prints the recovery command for
each. Nothing is printed for the normal ahead-of-main push.

Naming the PR directly would be better, but this environment's egress proxy
intercepts api.github.com and answers 403 ("GitHub access is not enabled for
this session"), so a shell hook cannot ask. What it can compute locally is
enough to stop the wrong claim being made.

Matched on Bash rather than the hook's `if` filter: that is a PREFIX match and
would miss `git add … && git commit … && git push …`, which is how most of these
pushes are actually run. The script exits immediately on a non-push command.

Both hooks output through jq as JSON so the message reaches the user
(systemMessage) and the model's context (additionalContext) — the latter is
where the incorrect claim was made.

Verified by running session-start.sh directly and by firing the PostToolUse hook
with a temporary sentinel. The branch check earned its place immediately: it
found that d867785 (this branch's upload-section commit) was never in PR #193,
which merged at c6bd29c, and that restarting the branch from main had moved off
it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg
ako added a commit that referenced this pull request Aug 20, 2026
chore(hooks): catch a stale checkout and a merged-PR push, plus the REST upload docs #193 missed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants