Summary
The generated OpenAPI HTTP client cannot send multipart/form-data. Any operation that uploads a file is ungeneratable today — the request body is always serialised as JSON.
Current behaviour
src/codegen/generators/typescript/channels/protocols/http/client.ts:
- Lines 170-172 — the body is always
const body = context.payload?.marshal();. marshal() produces a JSON string from the Modelina model; there is no FormData path anywhere in the HTTP generator.
- Line 167 — request headers hardcode
'Content-Type': 'application/json'.
The only non-JSON request encoding in the whole HTTP generator is application/x-www-form-urlencoded, and it exists solely for the internal OAuth token exchange (security.ts:523, :577) — it isn't reachable from generated operations.
The result is that an operation declared as requestBody: content: multipart/form-data: generates a function that sends a JSON string with the wrong Content-Type. The server rejects it. Nothing in the generator warns the user.
Why this matters
File upload is a common operation in public APIs: document submission, identity verification, avatars, attachments, bulk imports, media ingestion. A generator that covers every verb and every auth scheme but cannot upload a file will be discarded during evaluation by anyone whose API has a single upload endpoint — and that's a large fraction of real specifications.
It's also a correctness problem rather than a missing nicety. We accept the spec, emit code without error, and produce requests the server cannot parse. Refusing to generate would at least be honest; silently emitting a broken call is the worst of the three options.
Proposed direction
Detect multipart/form-data (and, for completeness, application/x-www-form-urlencoded) on the request body in the OpenAPI input processor, and emit a FormData-based body preparation instead of marshal():
- Build
FormData from the payload model's properties, appending File/Blob/ReadableStream values directly and JSON-encoding nested object properties as OpenAPI's encoding rules describe.
- Do not set
Content-Type manually for multipart — fetch must set it so the boundary is generated correctly. This means line 167 needs to become conditional rather than unconditional, which is the main structural change.
- Properties typed as
string with format: binary should surface in the generated payload type as something a caller can actually pass (Blob | File | Uint8Array), not string.
The encoding object in OpenAPI also allows per-part content types and headers. Supporting the common case first (plain fields + binary parts) and leaving per-part encoding for a follow-up seems reasonable — worth deciding whether to error or ignore when an unsupported encoding is present, rather than silently dropping it.
Acceptance criteria
- An operation with a
multipart/form-data request body generates a function accepting file-like values and sends a correctly-encoded multipart request.
Content-Type is not hardcoded for multipart requests; the boundary is produced by the runtime.
format: binary properties are typed usefully in the generated payload model.
- JSON-bodied operations are unaffected.
- Covered at all three tiers per
CLAUDE.md, including a runtime test that uploads a real file and asserts the server received the parts intact.
Following "Expected Output First", the desired generated signature for an upload operation should be written by hand in test/runtime/typescript/ before the generator work starts — the ergonomics of the payload type are the hard part here, not the encoding.
Depends on / relates to
Shares the conditional-Content-Type plumbing with #457 (non-JSON responses). Whichever lands first should make line 167 content-type-aware. #459 (SSE responses) is the third part of the same content-type gap.
Summary
The generated OpenAPI HTTP client cannot send
multipart/form-data. Any operation that uploads a file is ungeneratable today — the request body is always serialised as JSON.Current behaviour
src/codegen/generators/typescript/channels/protocols/http/client.ts:const body = context.payload?.marshal();.marshal()produces a JSON string from the Modelina model; there is noFormDatapath anywhere in the HTTP generator.'Content-Type': 'application/json'.The only non-JSON request encoding in the whole HTTP generator is
application/x-www-form-urlencoded, and it exists solely for the internal OAuth token exchange (security.ts:523,:577) — it isn't reachable from generated operations.The result is that an operation declared as
requestBody: content: multipart/form-data:generates a function that sends a JSON string with the wrongContent-Type. The server rejects it. Nothing in the generator warns the user.Why this matters
File upload is a common operation in public APIs: document submission, identity verification, avatars, attachments, bulk imports, media ingestion. A generator that covers every verb and every auth scheme but cannot upload a file will be discarded during evaluation by anyone whose API has a single upload endpoint — and that's a large fraction of real specifications.
It's also a correctness problem rather than a missing nicety. We accept the spec, emit code without error, and produce requests the server cannot parse. Refusing to generate would at least be honest; silently emitting a broken call is the worst of the three options.
Proposed direction
Detect
multipart/form-data(and, for completeness,application/x-www-form-urlencoded) on the request body in the OpenAPI input processor, and emit aFormData-based body preparation instead ofmarshal():FormDatafrom the payload model's properties, appendingFile/Blob/ReadableStreamvalues directly and JSON-encoding nested object properties as OpenAPI'sencodingrules describe.Content-Typemanually for multipart —fetchmust set it so the boundary is generated correctly. This means line 167 needs to become conditional rather than unconditional, which is the main structural change.stringwithformat: binaryshould surface in the generated payload type as something a caller can actually pass (Blob | File | Uint8Array), notstring.The
encodingobject in OpenAPI also allows per-part content types and headers. Supporting the common case first (plain fields + binary parts) and leaving per-partencodingfor a follow-up seems reasonable — worth deciding whether to error or ignore when an unsupportedencodingis present, rather than silently dropping it.Acceptance criteria
multipart/form-datarequest body generates a function accepting file-like values and sends a correctly-encoded multipart request.Content-Typeis not hardcoded for multipart requests; the boundary is produced by the runtime.format: binaryproperties are typed usefully in the generated payload model.CLAUDE.md, including a runtime test that uploads a real file and asserts the server received the parts intact.Following "Expected Output First", the desired generated signature for an upload operation should be written by hand in
test/runtime/typescript/before the generator work starts — the ergonomics of the payload type are the hard part here, not the encoding.Depends on / relates to
Shares the conditional-
Content-Typeplumbing with #457 (non-JSON responses). Whichever lands first should make line 167 content-type-aware. #459 (SSE responses) is the third part of the same content-type gap.