Skip to content

[Feature]: HTTP client should honour response content types other than application/json #457

Description

@ALagoni97

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions