Describe the feature
Summary
When a procedure returns binary data, the typed client (OpenAPILink / RPCLink) deserializes the HTTP response into a Blob/File, which reads the entire body into memory before handing it back. There is currently no way to receive the response body as a lazy ReadableStream<Uint8Array> through the typed client.
This makes it impossible to consume a streaming binary response progressively (e.g. feeding chunks into MediaSource as they arrive). The only true client-side streaming primitive is the Event Iterator (SSE), which base64-encodes binary chunks and adds framing overhead — not suitable for low-latency raw audio.
I'd like to request first-class support for a pass-through streaming response body on the client.
Use case
Streaming TTS audio for live playback:
- Server streams encoded audio bytes (e.g.
audio/mpeg) as the response body.
- Client should append chunks to a
MediaSource SourceBuffer as they arrive, so playback can start before the full clip is downloaded.
For this, the client needs the raw response.body (ReadableStream<Uint8Array>), not a buffered Blob.
Current behavior
Contract (binary response via outputStructure: 'detailed'):
readMessage: oc
.route({ method: 'POST', path: '/chats/read-message', outputStructure: 'detailed' })
.input(z.object({ messageId: z.uuid() }))
.output(
z.object({
headers: z.object({ 'content-type': z.string() }),
body: z.custom<ReadableStream<Uint8Array>>(),
}),
)
Client:
const { headers, body } = await client.chats.readMessage({ messageId })
// `body` is a fully-downloaded Blob, not a lazy ReadableStream.
// The whole response is buffered before this line resolves.
Observations:
z.custom<ReadableStream<Uint8Array>>() only affects TypeScript types; at runtime the link still produces a buffered Blob/File.
- Using a lazy file on the server (e.g.
@mjackson/lazy-file, Bun.file) helps server memory and time-to-first-byte over the wire, but the client still buffers the whole body into a Blob.
- Event Iterator works through the client but is SSE + base64 (≈ +33% size, per-chunk decode, framing latency), which is wasteful for raw audio.
Expected / proposed behavior
A first-class way to receive the response body lazily through the typed client, without buffering. Some possible shapes (open to whatever fits the architecture best):
- A link option or per-call client-context flag, e.g.
responseType: 'stream', that returns the raw Response / response.body and skips body deserialization.
- Treating
ReadableStream (and/or Response) as a native, pass-through output type that the codec does not consume.
- A documented, supported escape hatch on
OpenAPILink / RPCLink to obtain the raw Response for a given call while still using the contract to build the request (URL, method, headers, body serialization).
Ideally the request would still be built from the contract (so auth headers, URL, and input serialization stay consistent with the rest of the client), and only the response side would be left unparsed.
Current workaround
Bypassing the typed client for this one call with a plain fetch works, but loses the contract integration on the response side:
const res = await fetch(`${BASE_URL}/chats/read-message`, {
method: 'POST',
headers: { 'content-type': 'application/json' /* + auth */ },
body: JSON.stringify({ messageId }),
signal,
})
const stream = res.body // real ReadableStream<Uint8Array>
It's also possible to use the fetch option of OpenAPILink as a "request builder" (capture the built Request, then fetch it manually and read response.body), but that relies on aborting the pipeline before deserialization and feels like a hack rather than an intended API.
What I already checked
- The RPC protocol exposes streaming only via
AsyncIteratorObject (event iterator); binary values are Blob/File, which are read fully on decode. There is no pass-through ReadableStream output type.
- Discussion How to serve json file using ORPC #1052 confirms the intended file-download path is returning a
File directly — i.e. a buffered File, not a lazy stream.
- The server-side lazy-file recommendation (
@mjackson/lazy-file, Bun.file) reduces server memory but does not change client-side buffering.
I couldn't find an existing issue/discussion covering raw streaming response bodies on the client; apologies if I missed one.
Questions
- Is raw streaming response support already planned (the React Native docs reference a "Support Stream" tracking item)? If so, is there an issue to follow?
- Is there a recommended/supported way to get
response.body through the typed client today that I've missed?
Additional information
Describe the feature
Summary
When a procedure returns binary data, the typed client (
OpenAPILink/RPCLink) deserializes the HTTP response into aBlob/File, which reads the entire body into memory before handing it back. There is currently no way to receive the response body as a lazyReadableStream<Uint8Array>through the typed client.This makes it impossible to consume a streaming binary response progressively (e.g. feeding chunks into
MediaSourceas they arrive). The only true client-side streaming primitive is the Event Iterator (SSE), which base64-encodes binary chunks and adds framing overhead — not suitable for low-latency raw audio.I'd like to request first-class support for a pass-through streaming response body on the client.
Use case
Streaming TTS audio for live playback:
audio/mpeg) as the response body.MediaSourceSourceBufferas they arrive, so playback can start before the full clip is downloaded.For this, the client needs the raw
response.body(ReadableStream<Uint8Array>), not a bufferedBlob.Current behavior
Contract (binary response via
outputStructure: 'detailed'):Client:
Observations:
z.custom<ReadableStream<Uint8Array>>()only affects TypeScript types; at runtime the link still produces a bufferedBlob/File.@mjackson/lazy-file,Bun.file) helps server memory and time-to-first-byte over the wire, but the client still buffers the whole body into aBlob.Expected / proposed behavior
A first-class way to receive the response body lazily through the typed client, without buffering. Some possible shapes (open to whatever fits the architecture best):
responseType: 'stream', that returns the rawResponse/response.bodyand skips body deserialization.ReadableStream(and/orResponse) as a native, pass-through output type that the codec does not consume.OpenAPILink/RPCLinkto obtain the rawResponsefor a given call while still using the contract to build the request (URL, method, headers, body serialization).Ideally the request would still be built from the contract (so auth headers, URL, and input serialization stay consistent with the rest of the client), and only the response side would be left unparsed.
Current workaround
Bypassing the typed client for this one call with a plain
fetchworks, but loses the contract integration on the response side:It's also possible to use the
fetchoption ofOpenAPILinkas a "request builder" (capture the builtRequest, then fetch it manually and readresponse.body), but that relies on aborting the pipeline before deserialization and feels like a hack rather than an intended API.What I already checked
AsyncIteratorObject(event iterator); binary values areBlob/File, which are read fully on decode. There is no pass-throughReadableStreamoutput type.Filedirectly — i.e. a bufferedFile, not a lazy stream.@mjackson/lazy-file,Bun.file) reduces server memory but does not change client-side buffering.I couldn't find an existing issue/discussion covering raw streaming response bodies on the client; apologies if I missed one.
Questions
response.bodythrough the typed client today that I've missed?Additional information