perf(response): defer streams when copying response headers - #399
perf(response): defer streams when copying response headers#399colinhacks wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
The new header-copy path introduces an inconsistent default Content-Type value (text/plain;charset=UTF-8) relative to the repo’s established defaultContentType (text/plain; charset=UTF-8), making observable header defaults depend on the optimization path.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds an optimization hook to copy finalized response headers without eagerly materializing a ReadableStream, while preserving alias/consumption semantics for buffered string/null bodies (to match native shared-body behavior when a response is observed or re-sent).
Changes:
- Introduces a
Symbol.for('hono.response.copyHeaders')hook on the lightweightResponseto clone headers and defer body stream construction for eligible cached responses. - Tracks shared-body groups to ensure header-copy aliases observe consistent consumption/materialization semantics (including capping wrapper fanout).
- Updates the Node listener fast path to record “first send” consumption for shared bodies and fall back to the native-response path for subsequent sends.
File summaries
| File | Description |
|---|---|
| test/response-copy.test.ts | Adds regressions for header-copy behavior, alias consumption, materialization triggers, and fallback cases. |
| src/response.ts | Implements shared-body grouping, header-copy hook, and materialization/consumption tracking for cached responses. |
| src/listener.ts | Integrates shared-body consumption tracking into the cache fast path and falls back appropriately on repeat sends. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if (!group) { | ||
| const headers = new Headers(cache[2] instanceof Headers ? cache[2] : init?.headers) | ||
| if (cache[1] !== null && !headers.has('content-type')) { | ||
| headers.set('content-type', 'text/plain;charset=UTF-8') | ||
| } |
| expect(replacement.headers.get('x-saved')).toBeNull() | ||
| expect(original.headers.get('x-after')).toBeNull() | ||
| expect(replacement.headers.get('content-type')).toBe('text/plain;charset=UTF-8') | ||
| expect(await replacement.text()).toBe('héllo') |
|
The Node 24 failure is in the existing partial-body test ( |
Preserve buffered string/null bodies when Hono copies a finalized response to update its headers. The optional constructor hook copies headers without constructing a ReadableStream, and the cached writer records consumption so saved response aliases cannot replay the body.
Body observation materializes native responses sharing the same stream. Unsupported responses use the existing path, including streams, mutable byte arrays, subclasses, proxies and already-observed bodies. Shared groups are capped at 64 wrappers.
The companion Hono change, honojs/hono#5350, calls the hook before reading the body. Without it, the adapter retains the existing behavior. No application changes or runtime-specific condition are required.
Performance
Runnable reproduction, custom fixtures and raw results: hono-response-copy-benchmark. The repository builds the exact source revisions below and includes a second clean-VM verification run. These are custom fixtures, not Hono's official benchmark suite.
With both PRs applied, mixed-route throughput improved 65%, eight-header throughput 62%, and 64 KiB response throughput 31%. Application code was identical across controls. These gains require both the Hono hook and the adapter implementation; neither PR alone enables the fast path.
Median requests/second:
Node 26.8.1 on a dedicated Linux GCE n2-standard-8 VM. HTTP/1.1 loopback, 32 connections, two wrk threads, server and client pinned to separate physical cores. Three interleaved repetitions, each with a two-second warmup and five-second measurement. Mixed traffic was 80% middleware-backed parameter routes across 16 routes and 4,096 IDs, 10% text and 10% JSON. At two connections, mixed throughput improved from 14,973 to 25,679 requests/second (+71%).
The comparison used source-built Hono
eebdf7band adapter64dc09eas controls, versus Honob0855efand adapter066bf86. Subsequent commits add tests only. These are short, closed-loop fixture measurements, not production latency or application-wide performance claims.Tests