Skip to content

node:http: ServerResponse write()/end() observe a handler-destroyed socket - #35207

Open
robobun wants to merge 4 commits into
mainfrom
claude/f84dbd15/http-res-write-after-socket-destroy
Open

node:http: ServerResponse write()/end() observe a handler-destroyed socket#35207
robobun wants to merge 4 commits into
mainfrom
claude/f84dbd15/http-res-write-after-socket-destroy

Conversation

@robobun

@robobun robobun commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

What

Three ServerResponse.prototype.end() paths returned without ever setting finished / writableEnded, and write() on a dead socket reported true:

  1. A request handler calls req.socket.destroy() and then writes to the response: res.write() returns true (reporting the bytes as buffered) and res.end() is a silent no-op that leaves finished / writableEnded at false.
  2. A standalone new ServerResponse(req) (no native handle; light-my-request / fastify inject) that calls writeHead() before end(): the !handle fallback returned this without setting finished once _header was rendered.

Repro

import http from 'node:http';
const log = [];
const srv = http.createServer((rq, rs) => {
  srv.close(); rq.socket.destroy();
  const w = rs.write('body'); rs.end('done');
  log.push('write=' + w + ' finished=' + rs.finished + ' writableEnded=' + rs.writableEnded);
});
await new Promise(r => srv.listen(0, '127.0.0.1', r));
try { await fetch(`http://127.0.0.1:${srv.address().port}/`); } catch {}
await new Promise(r => setTimeout(r, 150));
console.log(JSON.stringify(log));
node v26.3.0 : ["write=false finished=true writableEnded=true", ...]
bun (before) : ["write=true finished=false writableEnded=false", ...]
bun (after)  : ["write=false finished=true writableEnded=true", ...]
import { ServerResponse, IncomingMessage } from 'node:http';
const res = new ServerResponse(new IncomingMessage(null));
res.writeHead(403);
res.end('forbidden');
console.log(res.writableEnded);  // node: true, bun (before): false, bun (after): true

Cause

ServerResponse.prototype.end had an if (handle?.aborted) return this early return, and both write() and end() had a flags & closed_or_completed check that returned true without updating any writable-side state. Separately, the !handle branch skipped the OutgoingMessage.end() delegation when _header was already set (writeHead already called) and returned without setting finished.

Node.js's OutgoingMessage._writeRaw() returns false once the assigned socket is destroyed, and OutgoingMessage.end() sets finished = true unconditionally; on a destroyed socket the finish callback handed to _send is dropped by the conn.destroyed branch, so 'prefinish' fires but 'finish' never does.

Fix

In src/js/node/_http_server.ts:

  • write(): return false on a closed/completed native handle instead of true; the write callback is dropped (never invoked), matching Node.js.
  • end() with a closed/completed native handle: drop the redundant handle?.aborted early return. Set _header, dump the request, set finished = true, emit 'prefinish', and return this without calling into the native handle or scheduling 'finish' / the end callback. 'close' is emitted by the existing socket-close path.
  • end() with no native handle: always delegate to OutgoingMessage.prototype.end, so finished is set and the header/body are buffered into outputData regardless of whether writeHead() was called first. The previous fallback also invoked the end callback via nextTick, which Node.js does not do (it registers it on 'finish').

Verification

New test in test/js/node/http/node-http-server-abort-events.test.ts destroys req.socket from inside the handler, then asserts write() === false, finished === true, writableEnded === true, and the event sequence ['res.prefinish', 'res.close'] (no 'finish').

New test in test/js/node/http/node-http.test.ts covers the standalone writeHead() + end() path: finished / writableEnded are true, outputData is populated, and the end callback is not invoked via nextTick.

Both fail on main; both pass with this change and match Node v26.3.0. node-http.test.ts, node-http-backpressure.test.ts, node-http-transfer-encoding.test.ts, node-http-server-socket-end-drain.test.ts, and the test-http-outgoing-* / test-http-pipeline-* / test-http-server-response-standalone / test-http-writable-true-after-close parallel scripts all pass unchanged.

Fixes #25632


[review] gate passed · iteration 4 · 4 files touched

fails on main (without fix)
ASAN without fix: 2 failed, 1 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/node/http/node-http-server-abort-events.test.ts test/js/node/http/node-http.test.ts
bun test v1.4.0 (bb6251a9f)

test/js/node/http/node-http.test.ts:
(pass) node:http > createServer > hello world [444.02ms]
(pass) node:http > createServer > is not marked encrypted (#5867) [94.73ms]
(pass) node:http > createServer > request & response body streaming (large) [163.56ms]
(pass) node:http > createServer > request & response body streaming (small) [85.14ms]
(pass) node:http > createServer > listen should return server [22.86ms]
(pass) node:http > createServer > listen callback should be bound to server [28.03ms]
(pass) node:http > createServer > should use the provided port [90.48ms]
(pass) node:http > createServer > should assign a random port when undefined [28.29ms]
(pass) node:http > createServer > option method should be uppercase (#7250) [101.79ms]
(pass) node:http > response > set-cookie works with getHeader [7.93ms]
(pass) node:http > response > set-cookie works with getHeaders [10.96ms]
(pass) node:http > request > should no
... (truncated)

release without fix: 1 skipped
bun test v1.4.0-canary.1 (892d0a1c2)

test/js/node/http/node-http.test.ts:
(pass) node:http > createServer > hello world [9.44ms]
(pass) node:http > createServer > is not marked encrypted (#5867) [2.93ms]
(pass) node:http > createServer > request & response body streaming (large) [4.77ms]
(pass) node:http > createServer > request & response body streaming (small) [2.69ms]
(pass) node:http > createServer > listen should return server [0.77ms]
(pass) node:http > createServer > listen callback should be bound to server [1.77ms]
(pass) node:http > createServer > should use the provided port [1.48ms]
(pass) node:http > createServer > should assign a random port when undefined [1.17ms]
(pass) node:http > createServer > option method should be uppercase (#7250) [2.49ms]
(pass) node:http > response > set-cookie works with getHeader [0.09ms]
(pass) node:http > response > set-cookie works with getHeaders [0.11ms]
(pass) node:http > request > should not insert extraneous accept-encoding header [2.30ms]
(pass) node:http > request > multiple Set-Cookie headers works #6810 [13.21ms]
(pass) node:http > request > should make a standard GET request when passed string as first arg [5
... (truncated)
passes on PR (with fix)
ASAN with fix: 1 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/node/http/node-http-server-abort-events.test.ts test/js/node/http/node-http.test.ts
bun test v1.4.0 (bb6251a9f)

test/js/node/http/node-http.test.ts:
(pass) node:http > createServer > hello world [476.61ms]
(pass) node:http > createServer > is not marked encrypted (#5867) [76.52ms]
(pass) node:http > createServer > request & response body streaming (large) [130.56ms]
(pass) node:http > createServer > request & response body streaming (small) [88.37ms]
(pass) node:http > createServer > listen should return server [37.56ms]
(pass) node:http > createServer > listen callback should be bound to server [38.11ms]
(pass) node:http > createServer > should use the provided port [82.40ms]
(pass) node:http > createServer > should assign a random port when undefined [45.49ms]
(pass) node:http > createServer > option method should be uppercase (#7250) [96.79ms]
(pass) node:http > response > set-cookie works with getHeader [7.26ms]
(pass) node:http > response > set-cookie works with getHeaders [10.63ms]
(pass) node:http > request > should not
... (truncated)

release with fix: 1 skipped
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 992ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/136] gen bindgenv2
[2/136] gen cpp.rs (cppbind)
[3/136] gen BunProcess.lut.h
Generating /workspace/bun/build/release/codegen/BunProcess.lut.h from /workspace/bun/src/jsc/bindings/BunProcess.cpp
[4/136] gen generated_host_exports.rs
generated_host_exports.rs: 91 exports (host=3, lazy=10, generic=78, rust=0); 237 extern-C blocks audited
[5/136] gen ZigGeneratedClasses.{cpp,h,rs}
Found 2 classes from /workspace/bun/src/jsc/resolve_message.classes.ts
  - ResolveMessage (13 fields)
  - BuildMessage (10 fields)
Found 1 classes from /workspace/bun/src/runtime/api/Archive.classes.ts
  - Archive (4 fields, 1 class fields)
Found 2 classes from /workspace/bun/src/runtime/api/BunObject.classes.ts
  - ResourceUsage (8 fields)
  - Subprocess (20 fields)
Found 1 classes from /workspace/bun/src/runtime/api/cron.classes.ts
  - CronJob (5 fields)
Found 3 classes from /workspace/bun/src/runtime/api/filesystem_router.classes.ts
  - FileSystemRouter (5 fields)
  - FrameworkFileSystemRouter (2 fields)
  - MatchedRoute (8 fie
... (truncated)
diff hotspot
src/js/node/_http_server.ts                        | 49 +++++++++--------
 test/js/node/http/node-http-proxy.js               |  4 +-
 .../http/node-http-server-abort-events.test.ts     | 61 ++++++++++++++++++++++
 test/js/node/http/node-http.test.ts                | 29 ++++++++++
 4 files changed, 118 insertions(+), 25 deletions(-)

gate history · 3 passed · 2 rejected · iteration 4

evidence per changed file
file                                                     reads  edits  tests
src/js/node/_http_server.ts                                 10      5      0
test/js/node/http/node-http-proxy.js                         1      1      0
test/js/node/http/node-http-server-abort-events.test.ts      2      2      0
test/js/node/http/node-http.test.ts                          3      3      0

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

ServerResponse completion handling now matches Node-like behavior for standalone responses and closed native handles. New tests cover lifecycle events, state transitions, output buffering, and callback timing. The proxy test uses 127.0.0.1 for binding and routing.

ServerResponse completion

Layer / File(s) Summary
Update response completion logic
src/js/node/_http_server.ts
end() completes state transitions for standalone and closed handles, while write() returns false after the native handle is closed.
Validate response lifecycle behavior
test/js/node/http/node-http-server-abort-events.test.ts, test/js/node/http/node-http.test.ts
Tests verify abort events, response state flags, buffered output, and end-callback timing.

Proxy test host normalization

Layer / File(s) Summary
Use numeric loopback routing
test/js/node/http/node-http-proxy.js
The proxy server and upstream request target 127.0.0.1 instead of localhost.

Possibly related PRs

Suggested reviewers: cirospaciari

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title matches the main change: ServerResponse write()/end() behavior for destroyed sockets.
Description check ✅ Passed It includes what changed and how it was verified, though the headings differ from the template.

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 12:51 AM PT - Jul 23rd, 2026

@robobun, your commit bb6251a9f7eef23688791547892183b738f8ce5c passed in Build #78378! 🎉


🧪   To try this PR locally:

bunx bun-pr 35207

That installs a local version of the PR into your bun-35207 executable, so you can run:

bun-35207 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 2 issues this PR may fix:

  1. ServerResponse.end() should always result in writableEnded being set to/returning true #25632 - PR directly fixes end() not setting writableEnded = true when the socket is already destroyed
  2. http.ServerResponse emits neither "finish" nor "close" after aborted request #30571 - PR partially addresses this by making end() set finished/writableEnded and emit prefinish after client abort (the missing close event is a separate socket-layer issue)

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #25632
Fixes #30571

🤖 Generated with Claude Code

@robobun

robobun commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

Checked the two suggested issues against main (e72299b) and this branch:

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I didn't find any bugs, but this changes observable behavior in ServerResponse.prototype.write() / end() — the most heavily exercised path in the node:http compat layer — so it's worth a human look.

What was reviewed:

  • Verified handle.abortedFlags::SOCKET_CLOSED (NodeHTTPResponse.rs:764), so it is a strict subset of closed_or_completed and the removed early return is indeed redundant.
  • Checked that hasServerResponseFinished (via this.finished) prevents a second end() from re-emitting 'prefinish'.
  • Confirmed the new closed-path block mirrors the existing fall-through block (same _header/req._dump()/markResponseEndedNT sequence, minus 'finish'/_callPendingCallbacks).
  • Test awaits real events, no sleeps, placed alongside the related abort-events test.
Extended reasoning...

Overview

Two files: src/js/node/_http_server.ts (~18 lines net) and a new test case in test/js/node/http/node-http-server-abort-events.test.ts. The change makes ServerResponse.prototype.write() return false (instead of true) and ServerResponse.prototype.end() transition to finished + emit 'prefinish' (instead of returning true with no state change) when the native handle's closed_or_completed flag is set. It also removes the handle?.aborted early return at the top of end().

Security risks

None. This is a Node.js-compat state-machine fix with no user-controlled parsing, auth, or crypto involvement.

Level of scrutiny

High. ServerResponse.prototype.write and end are the hot path for every node:http server response. The change is small but alters three observable behaviors: write() return value on a dead socket, end() return value (truethis — the old value was outright wrong), and event emissions on the closed path. The closed_or_completed mask covers both socket_closed and request_has_completed; only the former is exercised by the new test, and removing the handle?.aborted early return means callWriteHeadIfObservable and the trailer block now run before the closed check where they previously didn't on an aborted handle.

Other factors

The reasoning against Node.js's OutgoingMessage._writeRaw / end() is sound and cited in-source. I confirmed on the native side that get_aborted returns SOCKET_CLOSED, so the removed guard is provably subsumed by the later closed_or_completed check. The new closed-path block is a copy of the existing fall-through block minus the 'finish' / callback / _callPendingCallbacks scheduling, which matches the described Node behavior (the finish callback is dropped by _writeRaw's conn.destroyed branch). The PR reports the surrounding node-http suites and test-http-outgoing-* / test-http-writable-true-after-close parallel scripts pass unchanged. Given the surface area, a maintainer familiar with the node:http state machine should sign off.

Comment thread test/js/node/http/node-http.test.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/js/node/http/node-http-server-abort-events.test.ts`:
- Around line 71-75: Update the response write/end assertions around res.write
and res.end to pass callbacks, verify both callbacks remain uncalled after
res.close, and replace the weak endResult type assertion with an identity
assertion that endResult is res. Apply the same checks to the corresponding
second test location.

In `@test/js/node/http/node-http.test.ts`:
- Around line 2882-2884: Replace the explanatory regression comment near the
writeHead()/end() regression test with only the issue URL for `#25632`, removing
the surrounding behavioral explanation.
- Around line 2888-2896: Update the response assertion around res.outputData to
verify its buffered content includes the rendered 403 header block and response
body, rather than only checking that outputData.length is nonzero. Preserve the
existing finished and writableEnded assertions while replacing outputDataLength
with content-specific checks.
- Around line 2903-2908: Make the test containing the res2.end callback
assertion asynchronous, await one process.nextTick after calling res2.end, then
assert cbCalled directly rather than scheduling an unawaited next-tick
expectation. Preserve the existing finished and writableEnded assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ab978210-1d03-4278-b83d-df9c6f1de865

📥 Commits

Reviewing files that changed from the base of the PR and between e72299b and 2400a73.

📒 Files selected for processing (4)
  • src/js/node/_http_server.ts
  • test/js/node/http/node-http-proxy.js
  • test/js/node/http/node-http-server-abort-events.test.ts
  • test/js/node/http/node-http.test.ts

Comment thread test/js/node/http/node-http-server-abort-events.test.ts
Comment thread test/js/node/http/node-http.test.ts
Comment thread test/js/node/http/node-http.test.ts Outdated
Comment thread test/js/node/http/node-http.test.ts Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No bugs found, but this changes observable ServerResponse lifecycle behavior in three places (new 'prefinish' emission on a closed handle, end() callback no longer nextTick'd for standalone responses, write() return flipped to false), so it's worth a human look.

What was reviewed:

  • Checked that writableEnded derives from finished (_http_outgoing.ts:850), so the new finished = true covers both.
  • Checked that hasServerResponseFinished guards double-end() after the new closed-handle branch sets finished.
  • Checked the removed handle?.aborted early-return is subsumed by the closed_or_completed flag check (socket_closed | request_has_completed).
  • The req._dump() block in the new branch mirrors the existing success path at _http_server.ts:3224-3227 verbatim.
Extended reasoning...

Overview

The PR touches src/js/node/_http_server.ts (ServerResponse.prototype.end and .write), plus two test files and a hostname pin in a proxy test fixture. Three behavior changes: (1) write() on a closed/completed native handle now returns false instead of true; (2) end() on a closed/completed handle now sets _header, dumps the request, sets finished = true, schedules _ended, emits 'prefinish', and returns this (previously returned true — wrong type — with no state transition); (3) the !handle standalone-response branch now unconditionally delegates to OutgoingMessagePrototype.end instead of a conditional shortcut that skipped the state transition when _header was already set and invoked the callback via nextTick. The redundant handle?.aborted early return is removed.

Security risks

None. This is Node.js-compat state-machine bookkeeping on the response object; no auth, crypto, path handling, or untrusted-input parsing is touched.

Level of scrutiny

Medium-high. ServerResponse.prototype.end() is on the hot path of every node:http server, and each of the three changes alters observable behavior: a new synchronous 'prefinish' emission where none fired before, a return-value flip that backpressure-aware callers can observe, and a callback that standalone-response users (light-my-request / fastify inject) previously received via nextTick and now won't. All three are argued against Node v26.3.0 semantics with repros in the PR body, and the listed test suites (node-http*, test-http-outgoing-*, test-http-pipeline-*, test-http-server-response-standalone, test-http-writable-true-after-close) reportedly pass unchanged — but the blast radius of end() control-flow edits warrants a maintainer's eye on whether the removed handle?.aborted guard had any callers that reach it without closed_or_completed set, and whether emitting 'prefinish' synchronously from inside the closed-handle branch can re-enter anything.

Other factors

My earlier inline comment (nextTick assertion running outside the test's scope) and all four CodeRabbit findings were addressed in 892d0a1 and the threads are resolved. The new tests are well-shaped: the abort-events test awaits res 'close' before asserting, wires every failure to captured state, and asserts the full event sequence; the standalone test now awaits a nextTick before asserting cbCalled === false. CI build #78294 is in progress.

robobun added 4 commits July 23, 2026 03:17
…ocket

When a request handler calls `req.socket.destroy()` and then writes to
the response, ServerResponse.write() returned true and ServerResponse.end()
silently left finished/writableEnded false.

Node.js's OutgoingMessage._writeRaw() returns false once the assigned
socket is destroyed, and OutgoingMessage.end() still sets finished=true
and emits 'prefinish' (the 'finish' callback passed to _send is dropped
on the destroyed-socket branch, so 'finish' never fires).

Align the ServerResponse native-handle path with that:
- write(): return false on a closed/completed handle instead of true.
- end(): drop the redundant handle?.aborted early return; on a
  closed/completed handle set _header, dump the request, set
  finished=true, emit 'prefinish', and return `this` without calling
  into the native handle or scheduling 'finish'/the end callback.
…tableEnded

With no native handle (light-my-request, `new ServerResponse(req)`), calling
writeHead() then end() returned early without setting `finished` because the
`!handle` branch only delegated to OutgoingMessage.end() when `_header` was
unset. Route through OutgoingMessage.end() unconditionally so the end() state
transition and outputData buffering match Node.js.

Fixes #25632
The proxy fixture listened on and dialled `localhost`, so on a host whose
resolver prefers IPv6 the server binds `::1` and the client dials
`127.0.0.1` (or vice versa), refusing the connect. Pin both ends to
`127.0.0.1` to match the `exampleSite` it talks to.
- abort-events test: pass callbacks to write()/end() and assert both remain
  uncalled after 'close'; assert end() returned `res` itself.
- standalone #25632 test: make async and await one nextTick before asserting
  the end() callback was not invoked; assert the rendered 403 header block
  and body are buffered in outputData rather than just a nonzero length.
@robobun
robobun force-pushed the claude/f84dbd15/http-res-write-after-socket-destroy branch from 156378c to bb6251a Compare July 23, 2026 03:20

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No bugs found, and my earlier nit (unawaited nextTick assertion) was addressed. Deferring to a human because this inverts observable Node-compat behavior in _http_server.ts: write() on a closed handle now returns false (previously true, with a comment claiming that matched Node), end() on a closed handle now emits 'prefinish' and runs req._dump(), and the handle?.aborted early-return is gone so more of the end() path (trailers, callWriteHeadIfObservable) executes before bailing.

Checked: the 'prefinish'-on-destroyed-socket emit was compared against Node's OutgoingMessage.end and matches; the req._dump() block is copied verbatim from the fall-through path; test-http-writable-true-after-close asserts res.writable, not the write() return, so the true→false flip doesn't affect it.

Extended reasoning...

Overview

The PR touches src/js/node/_http_server.ts (ServerResponse.prototype.write/end), plus two test files and a localhost→127.0.0.1 fix in node-http-proxy.js. Three behavior changes:

  1. write() on a closed_or_completed native handle returns false instead of true (callback dropped).
  2. end() on a closed_or_completed handle sets _header, dumps req, sets finished, emits 'prefinish', and returns this — instead of the previous silent return true (which also had the wrong return type).
  3. end() with no native handle unconditionally delegates to OutgoingMessage.prototype.end, dropping the writeHead()-already-called shortcut and the process.nextTick(callback) shortcut.
  4. The if (handle?.aborted) return this early return at the top of end() is removed as redundant.

Security risks

None identified — this is state-machine bookkeeping in the Node HTTP compat layer, no auth/crypto/parsing of untrusted structure.

Level of scrutiny

High. _http_server.ts is one of the most stateful, event-order-sensitive files in the runtime, and this change inverts observable behavior that was previously commented as Node-matching ("node.js will return true if the handle is closed"). The new closed-handle end() branch adds a synchronous emit('prefinish') and a req._dump() call that weren't there before; both look correct per the cited Node semantics and are copied from the existing fall-through, but the interaction with pipelining, keep-alive teardown, and framework 'prefinish' listeners is the kind of thing a human familiar with this file should sign off on. The handle?.aborted removal means callWriteHeadIfObservable and the trailer path now run on an aborted handle before the closed_or_completed bail — likely fine (the trailer call is ?.-guarded), but worth a second pair of eyes.

Other factors

Tests are solid: the abort-events test awaits res.close, asserts the exact event sequence ['res.prefinish', 'res.close'] with no 'finish', and covers write-return, end-return-identity, callback suppression, and finished/writableEnded. The standalone-response test now awaits nextTick before asserting cbCalled === false (my earlier comment). PR body cites Node v26.3.0 verification and lists the full set of parallel/http tests that still pass. All prior review comments (mine and CodeRabbit's) are resolved.

@robobun

robobun commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator Author

Diff is green; CI noise is unrelated.

  • robobun/evidence gate passed on bb6251a: both new tests fail on main's src/ and pass with the fix, ASAN + release.
  • Builds #78294 and #78378: 47 lanes green each. darwin-14-aarch64-test-bun Expired on both (agent never picked the job up). Every test failure across both builds is [flaky]-tagged and in unrelated subsystems (webview-chrome, fs-promises FileHandle GC, in-process-cron, fetch-leak, bun-install-registry, proxy-stress, next-pages, valkey, fetch-backpressure, spawn, repl-close). None of my test files (node-http-server-abort-events.test.ts, node-http.test.ts) failed on any lane.

Ready for review; the remaining CI red is fleet/flake, not this change.

@robobun

robobun commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator Author

Re-checked against current main (165dc9f) now that #25632 is closed. Still needed: both tests added here fail on main unchanged.

  • node-http-server-abort-events.test.ts > res.write()/end() after req.socket.destroy() inside the handler: on main write() returns true, finished and writableEnded stay false, and the events are ["res.close"] with no prefinish. Node v26.3.0 gives write() === false, both flags true, ["res.prefinish", "res.close"] (checked with a standalone script as well).
  • node-http.test.ts > standalone ServerResponse end() after writeHead() sets writableEnded (#25632): on main finished and writableEnded are both still false after writeHead(403); end("forbidden").

What #25632 was closed on is the narrower case of a standalone response calling end() without writeHead() first, which does pass on main (#32488). The writeHead() then end() sequence, which is what fastify's reply path does, still reproduces, so the issue is not fully fixed. Leaving this open.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ServerResponse.end() should always result in writableEnded being set to/returning true

2 participants