Skip to content

Commit b6a88ad

Browse files
authored
feat(node): add batch response compression plugin (#1882)
Batch responses were never compressed. Their body frames several subresponses of mixed content types, so the envelope has no single content type for the runtime-agnostic Response Compression Plugin to judge, and a batch of JSON went out uncompressed even though it shrinks about tenfold. Compressing it with the web `CompressionStream` is not an option either: it cannot flush, so every early subresponse would sit in the compressor until the slowest one resolved, trading streaming for compression. `BatchResponseCompressionHandlerPlugin` in `@orpc/node` compresses them with `node:zlib` instead, ending every write with a sync flush. Each message reaches the client as soon as its procedure resolves, keep-alive frames included, so a streaming batch stays streaming while compressed. Registering the plugin is the explicit statement that your batches are mostly compressible, which is why it ships as an opt-in in the Node.js package rather than as default behaviour. Resolves #1645, which reached for the same goal by probing Node builtins from the shared fetch plugin via `process.getBuiltinModule`. ## Behaviour - Covers every successful batch response: the length-prefixed framing of a streaming batch or a buffered batch carrying binary, and the plain JSON array a buffered batch of JSON-only subresponses produces. - Leaves everything else alone: non-batch responses, whole-batch failures (`status >= 400`), bodies already content-encoded, partial `206` responses whose `Content-Range` would stop describing the body, and `Cache-Control: no-transform`. - Negotiates gzip, deflate, or deflate-raw from `Accept-Encoding`, keys shared caches with `Vary: accept-encoding`, and applies a size threshold where the size is known. - Composes with the Response Compression Plugin in either registration order, neither compressing what the other already did. - Nothing changes on the client: fetch implementations decompress as the stream arrives, so each message is decoded the moment it lands. ## Testing - A batch response measured 8.2 KB uncompressed against under 600 bytes on the wire. - The batch end-to-end suite gained a compressed variant over real HTTP with a real client, so its existing per-subresponse timing assertions now prove the compressor flushes rather than buffers. - A raw-socket test reads gzip bytes off the wire, and confirms keep-alive frames keep flowing while the batch is idle. - Every guard is covered by a test that fails when the guard is removed. ## Also `varyByAcceptEncoding` and `isNoTransformCacheControl` moved from the server plugin into `@orpc/shared`, with tests, so both compression plugins share one implementation. Two stale source links on the batch docs page now point at files that exist.
1 parent 81dec7f commit b6a88ad

14 files changed

Lines changed: 938 additions & 42 deletions

apps/content/docs/api-reference.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ For questions the reference does not answer, [oRPC on DeepWiki](https://deepwiki
4848
| [@orpc/swr](https://npmx.dev/package-docs/@orpc/swr) | Integrate with SWR. | [SWR](/docs/integrations/swr) |
4949
| [@orpc/experimental-effect](https://npmx.dev/package-docs/@orpc/experimental-effect) | Integrate with Effect. | [Effect](/docs/integrations/effect) |
5050
| [@orpc/nest](https://npmx.dev/package-docs/@orpc/nest) | Implement your contract with NestJS. | [NestJS](/docs/integrations/nest) |
51-
| [@orpc/node](https://npmx.dev/package-docs/@orpc/node) | Node.js plugins for static file serving and large uploads. | [Static File](/docs/plugins/static-file), [Tmp File Upload](/docs/plugins/tmp-file-upload) |
51+
| [@orpc/node](https://npmx.dev/package-docs/@orpc/node) | Node.js plugins for static file serving and large uploads. | [Static File](/docs/plugins/static-file), [Tmp File Upload](/docs/plugins/tmp-file-upload), [Batch Response Compression](/docs/plugins/batch-response-compression) |
5252
| [@orpc/bun](https://npmx.dev/package-docs/@orpc/bun) | Bun Redis adapters for Publisher and Rate Limit. | [Publisher](/docs/helpers/publisher), [Rate Limit](/docs/helpers/ratelimit) |
5353
| [@orpc/cloudflare](https://npmx.dev/package-docs/@orpc/cloudflare) | Cloudflare Durable Object and Rate Limit adapters. | [Publisher](/docs/helpers/publisher), [Rate Limit](/docs/helpers/ratelimit) |
5454
| [@orpc/trpc](https://npmx.dev/package-docs/@orpc/trpc) | Reuse existing tRPC routers within oRPC. | [tRPC](/docs/integrations/trpc) |
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
---
2+
title: "Batch Response Compression Plugin"
3+
description: "Compress batch responses when you know their contents compress, flushing each one as it is ready so a streaming batch stays streaming."
4+
sidebar:
5+
label: "Batch Response Compression"
6+
---
7+
8+
## Installation
9+
10+
```package-install
11+
npm install @orpc/node@beta
12+
```
13+
14+
## Setup
15+
16+
A [batch](/docs/plugins/batch) frames several subresponses together, each with its own content type, so the envelope has no single content type to judge. A batch of JSON compresses about tenfold; one carrying images or other already-compressed files does not compress at all, and nothing in the envelope says which you have. The [Response Compression Plugin](/docs/plugins/response-compression) therefore leaves framed batches alone rather than guess.
17+
18+
Use `BatchResponseCompressionHandlerPlugin` to compress them anyway. Registering it is how you state that your batches are compressible, which is usually the case when they carry JSON. It covers every successful batch response, whatever shape it takes.
19+
20+
```ts
21+
import { BatchResponseCompressionHandlerPlugin } from '@orpc/node'
22+
import { RPCHandler } from '@orpc/server/node'
23+
import { BatchHandlerPlugin } from '@orpc/server/plugins'
24+
25+
const handler = new RPCHandler(router, {
26+
plugins: [
27+
new BatchHandlerPlugin(),
28+
new BatchResponseCompressionHandlerPlugin({
29+
/**
30+
* The compression schemes to use for batch responses.
31+
* Schemes are prioritized by their order in this array and
32+
* only applied if the client supports them.
33+
* Supported values: 'gzip' | 'deflate' | 'deflate-raw'
34+
*
35+
* @default ['gzip', 'deflate']
36+
*/
37+
encodings: ['gzip', 'deflate'],
38+
39+
/**
40+
* The minimum response size in bytes required to trigger compression.
41+
* Responses smaller than this threshold will not be compressed to
42+
* avoid overhead. A streaming batch response has no size until it
43+
* ends, so it is always compressed.
44+
*
45+
* @default 1024 (1KB)
46+
*/
47+
threshold: 1024,
48+
}),
49+
],
50+
})
51+
```
52+
53+
:::warning
54+
The whole envelope is compressed, binary subresponses included. Leave this plugin off when your batches mostly carry images, video, or other already-compressed content: compressing it spends CPU for nothing and can make the response marginally larger.
55+
:::
56+
57+
## Why a Node.js Plugin
58+
59+
A streaming batch sends each response as soon as its procedure resolves. A compressor that cannot flush would hold every early response in its buffer until the slowest one finished, trading streaming for compression. The web [CompressionStream](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream) has no flush, so this plugin uses [zlib](https://nodejs.org/api/zlib.html) and ends each write with a sync flush instead. The few bytes each flush costs are what keep the batch streaming, [keep-alive frames](/docs/plugins/batch#keep-alive-timer) included.
60+
61+
## Client
62+
63+
No client setup is needed. Fetch implementations advertise the encodings they accept and decompress the response as it arrives, so the batch client decodes each message the moment it lands. For a link whose transport does not decompress on its own, add the [Response Compression Link Plugin](/docs/plugins/response-compression#client).
64+
65+
:::tip
66+
Register the [Response Compression Plugin](/docs/plugins/response-compression) alongside this one to cover everything else your handler serves. The two never compress the same response twice, whichever order they are registered in.
67+
:::
68+
69+
## Learn More
70+
71+
For implementation details, see the [source code](https://github.com/middleapi/orpc/blob/main/packages/node/src/batch-response-compression-handler-plugin.ts).

apps/content/docs/plugins/batch.mdx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,21 @@ const handler = new RPCHandler(router, {
106106
})
107107
```
108108

109+
## Compression
110+
111+
A batch response might be left uncompressed by the [Response Compression Plugin](/docs/plugins/response-compression), depending on the shape it takes. On Node.js, add the [Batch Response Compression Plugin](/docs/plugins/batch-response-compression) when you know your batch responses are all compressible. It covers every one of them, without giving up streaming:
112+
113+
```ts
114+
import { BatchResponseCompressionHandlerPlugin } from '@orpc/node'
115+
116+
const handler = new RPCHandler(router, {
117+
plugins: [
118+
new BatchHandlerPlugin(),
119+
new BatchResponseCompressionHandlerPlugin(),
120+
],
121+
})
122+
```
123+
109124
## Groups
110125

111126
Only requests in the same group are batched together. Each group also defines a context, as described in [client context](/docs/rpc/link#client-context).
@@ -173,4 +188,4 @@ const link = new RPCLink({
173188

174189
## Learn More
175190

176-
See the [BatchHandlerPlugin source code](https://github.com/middleapi/orpc/blob/main/packages/server/src/plugins/batch-handler-plugin.ts) and the [BatchLinkPlugin source code](https://github.com/middleapi/orpc/blob/main/packages/client/src/plugins/batch-link-plugin.ts) for implementation details.
191+
See the [BatchHandlerPlugin source code](https://github.com/middleapi/orpc/blob/main/packages/server/src/plugins/batch.ts) and the [BatchLinkPlugin source code](https://github.com/middleapi/orpc/blob/main/packages/client/src/plugins/batch.ts) for implementation details.

apps/content/docs/plugins/response-compression.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ const handler = new RPCHandler(router, {
4242
The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc/handler), [OpenAPIHandler](/docs/openapi/handler), or a custom one.
4343
:::
4444

45+
:::tip
46+
A [batch](/docs/plugins/batch) response might be left uncompressed here, depending on the shape it takes. On Node.js, add the [Batch Response Compression Plugin](/docs/plugins/batch-response-compression) to fully cover them, when you know your batch responses are all compressible.
47+
:::
48+
4549
## Client
4650

4751
Use `ResponseCompressionLinkPlugin` to advertise supported encodings via [Accept-Encoding header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Encoding) and automatically decompress response bodies based on the [Content-Encoding header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Encoding):

0 commit comments

Comments
 (0)