You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 9f12427
Browse filesBrowse the repository at this point in the historyBrowse files
Fix corrupt gzip from withNodeAdapter middleware on Node 26.11+, and return install_node_modules' npm report as an object on every npm (#3108) (#3393)
* Keep piped chunks on a patched write() under Node 26.11, and parse npm's report behind its dry-run diff
Node 26.11.0's Readable.pipe fast path (nodejs/node#66182) calls _write() directly
for a byte-mode Writable destination, skipping write() overrides. withNodeAdapter's
response is a real PassThrough, so compression's res.write patch was skipped for
send(...).pipe(res): corrupt gzip on the wire, or a stalled body. On runtimes with
the fast path the response gives each pipe source a second, inert 'data' listener,
which keeps pipe on dest.write(), and removes it on unpipe.
npm before 11.20 prints the human dry-run diff ahead of its --json report, so
install_node_modules returned that output as an unparsed string; npm 11.20 dropped
the diff and the dry_run tests' match on it. Parse the report after any prefix and
assert on its added count.
Claude-Session: https://claude.ai/code/session_013r4w8GFxG9nBkMVeRf7bbR
Dispatch-Task: main-red-kriszyp_harper_26a914501_38e57eff
* Release the pipe guard before pipe's own unpipe cleanup, so a stalled source stays paused
pipe's cleanup resumes a source awaiting 'drain' while any 'data' listener
remains, so with the guard's listener still attached an unpipe or a destroyed
response left the source flowing into nothing and its buffered data was lost.
Claude-Session: https://claude.ai/code/session_013r4w8GFxG9nBkMVeRf7bbR
Dispatch-Task: main-red-kriszyp_harper_26a914501_38e57eff
* Guard only core Readable sources, which legacy pipe() never releases
Legacy Stream.prototype.pipe emits 'pipe' but never 'unpipe', and neither it nor a
userland pipe() takes Node's fast path, so the inert listener is skipped for them.
Claude-Session: https://claude.ai/code/session_013r4w8GFxG9nBkMVeRf7bbR
Dispatch-Task: main-red-kriszyp_harper_26a914501_38e57eff
* Skip the pipe guard for sources the fast path never takes, and release it from one constructor listener
Object-mode and decoded sources cannot take Node 26.11's pipe fast path, so the
inert listener only cost them a call per chunk. One 'unpipe' listener prepended in
the constructor replaces the per-pipe release closure and still runs ahead of each
pipe's own cleanup.
Claude-Session: https://claude.ai/code/session_013r4w8GFxG9nBkMVeRf7bbR
Dispatch-Task: main-red-kriszyp_harper_26a914501_38e57eff
* Tolerate an 'unpipe' emitted without a source
Core and readable-stream always pass the source, but a bare res.emit('unpipe') from
third-party middleware must not throw out of the guard's release.
Claude-Session: https://claude.ai/code/session_013r4w8GFxG9nBkMVeRf7bbR
Dispatch-Task: main-red-kriszyp_harper_26a914501_38e57eff
* Float the Node 26 unit-test leg to the newest 26.x, as integration tests do
Without check-latest, setup-node prefers a cached 26.x, on which the pipe
fast-path regression test passes with the guard removed.
Claude-Session: https://claude.ai/code/session_013r4w8GFxG9nBkMVeRf7bbR
Dispatch-Task: main-red-kriszyp_harper_26a914501_38e57eff
* Check which package a dry run would add when npm reports it
npm 11's dry-run report names the packages in its add entries; npm 10's only counts
them, so the name is asserted when present.
Claude-Session: https://claude.ai/code/session_013r4w8GFxG9nBkMVeRf7bbR
Dispatch-Task: main-red-kriszyp_harper_26a914501_38e57eff
* Release the pipe guard only from a Readable, matching where it attaches
Claude-Session: https://claude.ai/code/session_013r4w8GFxG9nBkMVeRf7bbR
Dispatch-Task: main-red-kriszyp_harper_26a914501_38e57eff
---------
(cherry picked from commit b6b5edd)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: server/DESIGN.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -648,6 +648,7 @@ The SQL and job paths are additive rather than exclusive: `verifyPermsAST` valid
648
648
-**Headers commit exactly once, through `this.writeHead`.**`write()`, `end()`, `flushHeaders()` and `_implicitHeader()` all reach `this.writeHead(this.statusCode)` by property lookup, so a `writeHead` that `on-headers` replaced on the instance runs its listeners (the ones that set `Content-Encoding` and remove `Content-Length`) before the promise resolves. After commit, `setHeader`/`appendHeader`/`removeHeader` and a second `writeHead` throw `ERR_HTTP_HEADERS_SENT` as Node's do; `_header` (which `compression` ≤ 1.7 tests instead of `headersSent`) and `finished` (which `on-finished` tests) derive from that state.
649
649
-**The adapter owns the `'error'` listener.** A `destroy(err)` right after `writeHead()` emits before the awaiting caller can attach one; the error stays in the stream's `errored` state for `pipeline()`, `finished()` or async iteration. Client disconnect (`Request.signal`) destroys the response without an error after headers (a plain premature close, which `pipeBodyToResponse` treats as routine) and rejects the promise with the abort reason before them; a handler that throws or rejects before ending the response destroys it, and one that fails after `end()` is logged at warn.
650
650
-**Header names are case-insensitive on removal too.**`Headers.delete` lowercases like `set`/`get`/`has`; the inherited `Map.delete` silently left `send`'s `Content-Length` on a gzip body (truncated transfers).
651
+
- **Every piped chunk reaches `write()` by property lookup.** Node 26.11's `Readable.pipe` fast path (nodejs/node#66182) calls `_write()` directly when pipe's listener is a byte-mode source's only `'data'` listener, so `compression`'s instance patch of `res.write` was skipped for most chunks of `send(...).pipe(res)`: raw bytes inside the gzip stream, or a stall once `compression` had routed pipe's `'drain'` to its idle zlib stream. On runtimes that have the fast path (`'writeKnownBuffer' in Writable`, a static it added) the response adds an inert `'data'` listener to each byte-mode core `Readable` on `'pipe'`, which fails the fast path's single-listener check, and removes it on `'unpipe'` through a listener prepended in the constructor: pipe's own cleanup resumes a source awaiting `'drain'` while any `'data'` listener remains, which would leave an unpiped source flowing into nothing. Object-mode and decoded sources, and legacy `Stream.prototype.pipe` (which never emits `'unpipe'`), are skipped. Remove it once every supported Node 26 release dispatches pipe through an overridden `write()`.
651
652
-**Express is not a target.**`express`'s `app.handle()` replaces the response's prototype with one rooted at `http.ServerResponse.prototype`, which no Writable-derived response survives, and would do the same to the request `Proxy`'s target, Harper's real `IncomingMessage`. Middleware that duck-types the response (Next.js, `compression`, `send`, `serve-static`, `finalhandler`, h3, fastify) is the supported surface.
652
653
653
654
## `manageThreads` has two different `workerCount`s (`server/threads/manageThreads.js`)
awaitwaitUntil(()=>source.listenerCount('data')===0,"the source's 'data' listeners to be removed");
371
+
});
372
+
}
373
+
374
+
it('leaves a source paused with its data when its stalled pipe is unpiped or the response is destroyed, and no listener on any source',asyncfunction(){
0 commit comments