Repository navigation
fix(server): keep the response Content-Type when compressing Blob bodies - #2214
Conversation
For Blob/File bodies, ResponseCompressionHandlerPlugin ignored an existing Content-Type header: the compressibility check used `body.type` and the compressed response overwrote the header with `body.type`. The adapters only fall back to `body.type` when no header is set, so the same response got a different Content-Type depending on Accept-Encoding, and an untyped Blob with a compressible header was never compressed. Use the effective Content-Type (the existing header, falling back to `body.type`) for both the check and the output header. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfdK1RSJyFSUuXFgSefuah
The three tests differed only in the blob, the injected Content-Type header and the expected encoding, so they now share one `it.each` and a single copy of the header-injecting plugin. Drop the extra uncompressed request, which only re-checked adapter behaviour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfdK1RSJyFSUuXFgSefuah
Use `headers['content-type'] ?? body.type` as is, like the adapters' `??=`, and flatten it only for the compressibility check. Flattening first turned an empty-array header into `undefined` and fell back to the blob type, so a response the Node adapter sends without a Content-Type was compressed and sent with `body.type`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfdK1RSJyFSUuXFgSefuah
Using the existing Content-Type header is the expected behavior, so it does not need its own mention. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfdK1RSJyFSUuXFgSefuah
Cover all four cases in one table that sets the header with a handler routing interceptor, and check that a compressed response carries the same Content-Type as the uncompressed one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfdK1RSJyFSUuXFgSefuah
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
orpc | 5ae6f20 | Commit Preview URL Branch Preview URL |
Oct 13 2026, 07:04 AM |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Blob/File Content-Type preserved on compression —
ResponseCompressionHandlerPluginnow derives the content type asheaders['content-type'] ?? body.type, uses the flattened value only for the compressibility check, and writes the raw value to the output header, matching the adapters'??= body.typefallback. - Table-driven regression tests — one
it.eachindescribe('blob body')covers header-over-blob precedence, the charset-preservation case, theimage/pngno-compress case, and an empty-array header, asserting the compressedcontent-typeequals the uncompressed one.
I confirmed the adapters (@standard-server/fetch and /peer) do headers["content-type"] ??= body.type, and that flattenStandardHeader([]) returns undefined (so the empty-array row stays uncompressed and unset, matching the adapter). BatchResponseCompressionHandlerPlugin is correctly left alone — its Blob is the envelope it builds. Ran pnpm vitest run packages/server/src/plugins/response-compression.test.ts here: 56 passed. The request-side counterpart (RequestCompressionLinkPlugin) is a pre-existing gap the author already documented as out of scope. No actionable concerns.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
More templates
@orpc/ai-sdk
@orpc/arktype
@orpc/bun
@orpc/client
@orpc/cloudflare
@orpc/contract
@orpc/experimental-effect
@orpc/evlog
@orpc/hibernation
@orpc/json-schema
@orpc/experimental-lock
@orpc/experimental-msw
@orpc/nest
@orpc/next
@orpc/node
@orpc/openapi
@orpc/opentelemetry
@orpc/pinia-colada
@orpc/pino
@orpc/publisher
@orpc/ratelimit
@orpc/server
@orpc/shared
@orpc/swr
@orpc/tanstack-query
@orpc/trpc
@orpc/valibot
@orpc/zod
commit: |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Merging this PR will not alter performance
|

Problem
For a
Blob/Fileresponse body,ResponseCompressionHandlerPluginignored aContent-Typeheader the response already had. That header can come fromResponseHeadersPlugin'scontext.resHeaders, from OpenAPI detailed output headers, or from another interceptor. The plugin decided whether to compress frombody.type, then overwrote the header withbody.type. The adapters doheaders['content-type'] ??= body.type, so the header wins when the response is not compressed. The same response therefore got a different Content-Type depending onAccept-Encoding:text/csv; charset=utf-8text/plaintext/plaintext/html; charset=utf-8''''is not compressible)image/pngtext/plaintext/plainFix
In the Blob branch, the Content-Type is now
headers['content-type'] ?? body.type, the same fallback as the adapters'??=. That value is used as is for the output header and flattened only for theisCompressibleContentTypecheck, which needs a single string. Because the raw value is kept, an empty-array header still counts as set, as it does in the adapters (Node then sends no Content-Type), instead of falling back to the blob type.BatchResponseCompressionHandlerPluginin@orpc/nodeis unchanged. Its Blob is the batch envelope the plugin builds itself, sobody.typeis right there.Tests
One
it.eachindescribe('blob body')covers the three cases above, plus an empty-array header. Each row sets the header with a routing interceptor, then checks the encoding and that the compressed response has the same Content-Type as the uncompressed one. All four rows fail before the fix. The existing blob tests pass unchanged.Validation:
pnpm vitest runon the response compression and batch response compression tests (84 passed),pnpm type:checkandpnpm lintall pass.Note
RequestCompressionLinkPluginin@orpc/clienthas the same issue for request bodies (it usesrequest.body.typefor both the check and the header). It is not changed here.🤖 Generated with Claude Code
https://claude.ai/code/session_01KfdK1RSJyFSUuXFgSefuah
Generated by Claude Code