Conversation
Request bodies were only ever read from the `application/json` content of an
operation. For operations without it, the whole OpenAPI request body object was
handed to json-schema-to-typescript, which produced `{ [p: string]: unknown }`.
Consumers of `multipart/form-data` routes such as
`verification-detect-phishing-email` therefore had to cast their `File` to
`unknown` (#264).
The generator now picks `application/json` when the operation offers it and
falls back to the first declared media type otherwise, and maps binary schemas
(`type: string, format: binary` in OpenAPI 3.0, `contentMediaType:
application/octet-stream` / `contentEncoding: binary` in 3.1) to the new
`BinaryData` type exported by `@mittwald/api-client-commons`. `BinaryData` is
`Blob`, which covers browser `File`s from file inputs and drop zones as well as
Node's global `Blob`/`File`.
A nice type alone would not have helped: axios JSON-serialized multipart
payloads into `{"emailEml":{}}`, and its own multipart conversion falls back to
the `form-data` package on Node, which cannot handle `Blob`s. Operations whose
request body is not JSON now carry their media type in the generated descriptor
(`requestContentType`), and the runtime builds a real `FormData` for multipart
bodies and lets axios encode everything else once it knows the content type.
Payloads that are not plain objects (an already built `FormData`, a
`URLSearchParams`, a `Blob`, a stream) are passed through untouched, and an
explicitly set `Content-Type` request header always wins.
Affected operations in the generated client: `file-create-file`,
`verification-detect-phishing-email` (both multipart) and
`user-oauth-retrieve-access-token` (form-urlencoded).
Closes #264
BREAKING CHANGE: The request body types of the three non-JSON operations
(`file-create-file`, `verification-detect-phishing-email` and
`user-oauth-retrieve-access-token`) narrow from `{ [p: string]: unknown }` to
their declared shape, so code passing arbitrary extra keys no longer compiles.
Their bodies are now sent as `multipart/form-data` respectively
`application/x-www-form-urlencoded` instead of JSON.
Both behaviours were broken before: a multipart payload was JSON-serialized to
`{"emailEml":{}}` and the file was silently dropped, so these routes could not
be called at all without hand-rolling a `FormData` and casting it to `unknown`.
Callers who did that must remove the workaround and pass the `File`/`Blob`
directly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 4, 2026
Master's date-time PR (#299) rewrote the same three spots in Request.ts: - The request body is now taken from the date-serialized request object, so a `Date` in a multipart body is appended as a bare ISO string part rather than a JSON-quoted one. Covered by a new test. - `makeAxiosHeaders` keeps this branch's optional-headers/content-type handling and applies master's `serializeDates` to the header values. `serializeDates` rebuilds plain objects, so a JSON body is no longer the caller's exact object instance. Three assertions in requestBody.test.ts that checked referential identity now compare structurally; the non-plain payloads (FormData, Blob, URLSearchParams) are still passed through by reference. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #264
Warning
This is the only PR in the current batch marked breaking. The commit is
feat!:with aBREAKING CHANGE:footer, so merging it makes lerna-lite cut 5.0.0 for all packages. If you would rather not spend a major right now, say so and I will re-scope — see "The release decision" at the bottom.Why the type collapsed
RequestParameters.fromDoconly ever looked atapplication/json:For a multipart-only route the fallback handed the entire OpenAPI request body object (
{content: {...}, required: true}) to json-schema-to-typescript. That has notype/properties, so it compiled tointerface RequestBody { [k: string]: unknown }. Two independent defects: non-JSON media types were never modelled, andformat: binaryhad no TS mapping.The runtime was broken too — the type was the smaller half
Probed against real axios 1.15.2 and a local HTTP server: passing
{ emailEml: file }today producesContent-Type: application/jsonand body{"emailEml":{}}. The file silently vanishes. These routes could not be called at all without hand-rolling aFormDataand casting.Delegating to axios was not an option either — its
toFormDatauses theform-datapackage on Node, which rejectsBlob("Blob is not supported. Use a Buffer instead.") and crashes onFile(source.on is not a function). So the client builds theFormDataitself.The type
BinaryData = Blob, exported from@mittwald/api-client-commons.FileextendsBlob— exactly the issue's use case (file input / dropzone).Blob(18+) andFile(20+);@types/nodedeclares both, so consumers need no DOM lib.tsconfig.jsonalready sets"lib": ["ES2022", "dom"]and every package extends it, soBlob/File/FormDataresolve.BinaryDatais not a name inlib.dom.d.tsor@types/node, so no global collision.Buffer/Uint8Array/ReadableStream: WHATWGFormData.appendaccepts onlystring | Blob, so allowing them would type-check and then silently mangle the payload. Node users wrap:new Blob([buffer]).Covers OpenAPI 3.0 (
type: string, format: binary) and 3.1 (contentMediaType: application/octet-stream,contentEncoding: binary).format: byte/contentEncoding: base64staystring.Applied to request bodies only. Several routes have
format: binaryresponses (/v2/files/{fileId}, container-template assets) which axios returns as strings under the defaultresponseType— typing thoseBlobwould have been a lie.Runtime changes
New
packages/commons/src/core/requestBody.ts: only plain objects are treated as field bags (FormData,URLSearchParams,Blob,Buffer, streams and class instances pass through untouched); multipart builds a real globalFormData; noContent-Typeis set for multipart so axios owns the boundary; an explicitly suppliedContent-Typealways wins.OpenAPIOperationgained an optionalrequestContentType?: HttpMediaType(additive). The generator emits it only for non-JSON bodies, which is whydescriptors.tsgained exactly 3 lines rather than 270.Verified on the wire:
What exactly breaks
Three operations, both spec versions:
file-create-file,verification-detect-phishing-email(multipart),user-oauth-retrieve-access-token(form-urlencoded).export interface RequestBody { - [k: string]: unknown; + file: BinaryData; }additionalProperties: false— so an index signature here would be an inconsistent special case, and (b) the only code that can break is code that was already sending a broken request.Generated diff, regenerated in-PR: 60 insertions / 6 deletions across 4 files. Baseline regeneration was confirmed byte-identical to
masterfirst, and the post-change generation is idempotent.Tests
binarySchemasToCustomTypes.test.ts(3.0/3.1 forms, base64 negatives, nesting, non-mutation) ·RequestParameters.test.ts(fixture spec with a binary field plus a normal string field; assertsfile: BinaryData,description?: string, no index signature, the import, the descriptor'srequestContentType, JSON-preferred-when-present, urlencoded) ·requestBody.test.ts(12 serialization cases) ·packages/mittwald/src/v2/fileUpload.test-types.ts— a consumer-facing regression guard for this issue:File/Blobassignable without a cast,@ts-expect-erroron a string and on{}.All green:
nx run-many -t test --skip-nx-cache(4 projects, 9 dependent tasks), generatortest:unit(9 suites/39), commonstest(4 suites/33),test:compile(3),yarn lint(4).The release decision
The practical break is close to zero — the affected code paths were non-functional. If you would rather not cut 5.0.0 for this, the alternatives are: hold the PR until the next intended major, or split it so the runtime fix (
feat:) lands now and the type narrowing waits. I marked it breaking because it genuinely is by the letter of the contract, and silently shipping a narrowed public type as a minor is the worse failure mode.🤖 Generated with Claude Code