Skip to content

perf(response): defer streams when copying response headers - #399

Open
colinhacks wants to merge 2 commits into
honojs:mainfrom
colinhacks:perf-response-header-copy
Open

perf(response): defer streams when copying response headers#399
colinhacks wants to merge 2 commits into
honojs:mainfrom
colinhacks:perf-response-header-copy

Conversation

@colinhacks

@colinhacks colinhacks commented Sep 7, 2026

Copy link
Copy Markdown

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:

Workload Unmodified packages Both PRs Both PRs, hook disabled Change vs unmodified
Mixed routes and middleware 16,254 26,857 15,891 +65%
Eight post-handler headers 11,086 18,000 10,841 +62%
64 KiB string with middleware 8,086 10,625 7,511 +31%
Plain text, no middleware 39,897 39,223 39,516 −1.7%

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 eebdf7b and adapter 64dc09e as controls, versus Hono b0855ef and adapter 066bf86. Subsequent commits add tests only. These are short, closed-loop fixture measurements, not production latency or application-wide performance claims.

Tests

  • Added header-copy, alias-consumption, fallback and real HTTP GET/HEAD regressions.
  • Full suite: 448 passed on Node 20 and 22; 449 passed on Node 22 after adding the Content-Type regression. Typecheck, lint, format and ESM/CJS build passed.
  • Cross-package differential checks passed on Node 20, 22, 24 and 26; CJS integration passed.
  • Full suites on Node 24/26 retained one/three request partial-read or disconnect failures, respectively, reproduced on the unchanged base with the same dependencies.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 lightweight Response to 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.

Comment thread src/response.ts
Comment on lines +128 to +132
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')
}
Comment on lines +69 to +72
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')
@colinhacks

Copy link
Copy Markdown
Author

The Node 24 failure is in the existing partial-body test (request.test.ts:318), reproduced on the unchanged base 64dc09e as well. Neither the request implementation nor that test changes here. All 14 response-copy tests passed; the Node 20/22 jobs were cancelled by matrix fail-fast. The Windows suite and package build passed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants