DEV Community

Jeff
Jeff

Posted on Originally published at powerduck.com

Codegen for Binary Downloads: Streaming a File Response Instead of Decoding It as Text

Quick answer: A generated client that treats every response as a string will corrupt binary files like PDFs, images, and archives, because decoding raw bytes through a text codec mangles them. The correct generated return type for a binary media type is a stream, byte array, or blob depending on the language, and the OpenAPI format: binary keyword is what tells the generator to produce it. Verify the return type before you rely on a downloaded file, and never decode binary as UTF-8.

How the contract describes a binary response

OpenAPI distinguishes a JSON body from a binary one through the response content media type and schema. A file download uses a string schema with the binary format, and the media type reflects the actual file rather than JSON.

That single format: binary drives the generator's choice of return type. If the response is mislabeled as application/json or the format is omitted, the generator defaults to a text or object type and the download is wrong before any application code runs.

What different generators produce

The exact type changes with the target language, but the intent is consistent: hold bytes without text decoding.

Target Typical binary return type How you consume it
TypeScript / fetch Blob or ReadableStream Save with a download link or pipe the stream
Java byte[] or InputStream Write to a file or forward to the response
Python bytes or a file-like iterator Open in binary mode and write
Go io.ReadCloser io.Copy into a file, then close

Streaming instead of buffering

For a large file, loading the whole body into memory is wasteful and can exhaust a constrained process. A stream lets you write chunks to disk as they arrive. Here is a TypeScript path that uses the response stream and a Node path that copies bytes directly.

// Browser / fetch: stream into a file rather than buffering async function saveInvoice(id: string) { const res = await client.downloadInvoicePdf(id); const blob: Blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download =invoice-${id}.pdf; a.click(); URL.revokeObjectURL(url); } # Go: copy the reader straight to a file in binary mode func downloadInvoice(ctx context.Context, id, dest string) error { rc, err := client.DownloadInvoicePdf(ctx, id) if err != nil { return err } defer rc.Close() f, err := os.Create(dest) if err != nil { return err } defer f.Close() _, err = io.Copy(f, rc) return err }

Why decoding as text corrupts the file

Text codecs assume bytes represent valid encoded characters and apply replacement or reinterpretation when they do not. A PDF header or compressed archive contains arbitrary byte sequences that are not valid UTF-8, so a text decode substitutes characters, changes length, and produces a file that fails to open. The damage is silent; the call returns a 200 and the bug only appears when someone tries to open the saved file. This is why the return type matters more than the status code for download endpoints.

How an AI agent gets this wrong

An agent generating client code often reaches for the familiar res.json() or res.text() call because that is the common case, without checking the operation's media type. When asked to "add a download button," it may stringify the response and write it to a file, producing a corrupt download that looks complete. In review, find the line that reads the response body and confirm it matches the binary contract rather than the default JSON path. Require the agent to generate from the OpenAPI operation so the type is chosen by the contract, then add a test that opens the resulting bytes and checks the file signature, such as the leading %PDF marker.

Handling errors and headers on a binary route

An error response on a download endpoint is often JSON even though the success response is binary. The generated client needs to branch on content type: read a problem body as JSON when the status is an error, and read bytes only on success. Also preserve the Content-Disposition header if the server suggests a filename, rather than hardcoding one. A robust generated method exposes both the bytes and the headers so calling code does not have to re-implement the request to read them.

Frequently asked questions

What OpenAPI keyword controls the generated binary type?

Use type: string with format: binary under the correct binary media type such as application/pdf or application/octet-stream. The generator uses this to return a blob, byte array, or stream instead of a string.

Is it safe to use res.text() and then write the result to a file?

No. Reading bytes as text can reinterpret or replace invalid characters and corrupt the file. Keep the response as bytes or a stream and write it in binary mode.

Should I buffer the whole file or stream it?

Small files are fine as a blob or byte array. For large downloads, stream chunks to disk to avoid holding the entire file in memory, especially in a server or constrained environment.

How do I test that a downloaded file is valid?

Check the magic bytes for the expected type, such as %PDF for a PDF, and verify the file opens or has the expected size. A passing status code alone does not prove the bytes survived decoding.

This is part of the The Code Generation Guide — start there for the type and runtime pitfalls that generated clients hit.

Top comments (0)