Summary
The generated OpenAPI HTTP client assumes every response is JSON. Any operation whose response is binary (PDF, CSV, image, application/octet-stream) or plain text throws at runtime, even when the OpenAPI document describes the content type correctly.
Current behaviour
src/codegen/generators/typescript/channels/protocols/http/client.ts:
- Line 289 — every generated operation calls
const rawData = await response.json(); unconditionally. There is no branch on the response Content-Type.
- Lines 184-187 — the parsed object is then re-serialised with
JSON.stringify(rawData) and handed to unmarshal().
- Line 167 — the request headers hardcode
'Content-Type': 'application/json'.
So for a 200 response declared as application/pdf or text/csv, response.json() rejects and the operation fails. The hooks.makeRequest escape hatch does not help: it can replace the request function, but .json() is emitted into every generated function body, so a consumer cannot opt out of it.
Why this matters
Binary and text responses are ordinary in real specifications — invoice PDFs, shipping labels, CSV exports, generated reports, file downloads. A user who points the CLI at a spec containing one of these gets a client that compiles fine and then throws the first time that operation is called. That is a worse experience than a generator that refuses the operation outright, because the failure only shows up at runtime.
It also puts us behind on plain OpenAPI conformance: the response content type is right there in the document and we discard it. Anyone evaluating the CLI against a real-world spec is likely to hit this in their first hour, and "it can't download a file" is a hard stop for adoption regardless of how good the rest of the output is.
Proposed direction
Branch response handling on the declared response content type from the OpenAPI document, falling back to the runtime Content-Type header when the spec is ambiguous:
| Declared content type |
Suggested handling |
application/json, +json |
current path — .json() then unmarshal() |
text/* |
await response.text(), typed as string |
application/octet-stream, image/*, application/pdf, other binary |
await response.blob() or .arrayBuffer() |
| unknown / absent |
keep today's JSON behaviour so nothing regresses |
The response wrapper already carries rawData alongside data (client.ts:297-305), so there is a natural place to put the unparsed body without changing the shape of HttpClientResponse.
Open question worth deciding early: whether the return type varies per operation based on the spec (better DX, more generator work) or whether binary operations return a common Blob/ArrayBuffer type. I'd lean towards per-operation typing since the information is already in the document.
Acceptance criteria
- An operation with a non-JSON response no longer throws; it returns the body in an appropriate type.
- The generated types reflect the declared content type rather than always resolving to the JSON model.
- Existing JSON-only specs generate byte-identical output (no snapshot churn beyond intentional changes).
- Covered at all three tiers per
CLAUDE.md: unit snapshots, a blackbox config/input combination with a binary response, and a runtime test proving a real download round-trips.
Per the "Expected Output First" convention, it's probably worth hand-writing the desired client shape in test/runtime/typescript/ before touching the generator.
Related
This is the foundation for two sibling issues that plug into the same dispatch point: #459 (text/event-stream / SSE responses) and #458 (multipart request bodies, which needs the request-side Content-Type at line 167 to become conditional). Landing this one first is probably cheapest.
Partially overlaps #426 ("Error codes if they contain a content type are not generated?") — that issue is about error responses and naming; this one is about the success-path body. They may share the content-type plumbing.
Summary
The generated OpenAPI HTTP client assumes every response is JSON. Any operation whose response is binary (PDF, CSV, image,
application/octet-stream) or plain text throws at runtime, even when the OpenAPI document describes the content type correctly.Current behaviour
src/codegen/generators/typescript/channels/protocols/http/client.ts:const rawData = await response.json();unconditionally. There is no branch on the responseContent-Type.JSON.stringify(rawData)and handed tounmarshal().'Content-Type': 'application/json'.So for a
200response declared asapplication/pdfortext/csv,response.json()rejects and the operation fails. Thehooks.makeRequestescape hatch does not help: it can replace the request function, but.json()is emitted into every generated function body, so a consumer cannot opt out of it.Why this matters
Binary and text responses are ordinary in real specifications — invoice PDFs, shipping labels, CSV exports, generated reports, file downloads. A user who points the CLI at a spec containing one of these gets a client that compiles fine and then throws the first time that operation is called. That is a worse experience than a generator that refuses the operation outright, because the failure only shows up at runtime.
It also puts us behind on plain OpenAPI conformance: the response content type is right there in the document and we discard it. Anyone evaluating the CLI against a real-world spec is likely to hit this in their first hour, and "it can't download a file" is a hard stop for adoption regardless of how good the rest of the output is.
Proposed direction
Branch response handling on the declared response content type from the OpenAPI document, falling back to the runtime
Content-Typeheader when the spec is ambiguous:application/json,+json.json()thenunmarshal()text/*await response.text(), typed asstringapplication/octet-stream,image/*,application/pdf, other binaryawait response.blob()or.arrayBuffer()The response wrapper already carries
rawDataalongsidedata(client.ts:297-305), so there is a natural place to put the unparsed body without changing the shape ofHttpClientResponse.Open question worth deciding early: whether the return type varies per operation based on the spec (better DX, more generator work) or whether binary operations return a common
Blob/ArrayBuffertype. I'd lean towards per-operation typing since the information is already in the document.Acceptance criteria
CLAUDE.md: unit snapshots, a blackbox config/input combination with a binary response, and a runtime test proving a real download round-trips.Per the "Expected Output First" convention, it's probably worth hand-writing the desired client shape in
test/runtime/typescript/before touching the generator.Related
This is the foundation for two sibling issues that plug into the same dispatch point: #459 (
text/event-stream/ SSE responses) and #458 (multipart request bodies, which needs the request-sideContent-Typeat line 167 to become conditional). Landing this one first is probably cheapest.Partially overlaps #426 ("Error codes if they contain a content type are not generated?") — that issue is about error responses and naming; this one is about the success-path body. They may share the content-type plumbing.