Skip to content

node:http: gate server.close() on connection drain, not request count - #35844

Closed
robobun wants to merge 6 commits into
mainfrom
farm/0d035f14/http-server-close-connection-drain
Closed

node:http: gate server.close() on connection drain, not request count#35844
robobun wants to merge 6 commits into
mainfrom
farm/0d035f14/http-server-close-connection-drain

Conversation

@robobun

@robobun robobun commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Repro

import http from "node:http"; import net from "node:net";
const server = http.createServer((req, res) => setTimeout(() => res.end("resp:" + req.url), 50));
server.keepAliveTimeout = 60000;
server.listen(0, "127.0.0.1", () => {
  const c = net.connect(server.address().port, "127.0.0.1");
  let cbAt = null, n = 0, body = "";
  c.on("data", d => body += d);
  c.on("connect", () => {
    c.write("GET /0 HTTP/1.1\r\nHost: x\r\n\r\n");
    setTimeout(() => server.close(() => { cbAt = n }), 10);
    const id = setInterval(() => {
      n++;
      c.write("GET /" + n + " HTTP/1.1\r\nHost: x\r\n\r\n");
      if (n === 5) {
        clearInterval(id);
        setTimeout(() => {
          console.log({ cbFiredAt: cbAt, responses: (body.match(/resp:/g) || []).length });
          process.exit(0);
        }, 200);
      }
    }, 100);
  });
});
cbFiredAt responses
node null 6
bun 0 6

server.close()'s callback fires as soon as the first response is written, then five more requests are served on that same connection. Any server.close(() => process.exit()) style graceful drain is told the server is drained while a live keep-alive client is still issuing requests into it.

Cause

emitCloseNTServer is driven by getBunServerAllClosedPromise, whose condition (deinit_if_we_can) is pending_requests == 0 && !listener && !websockets. That is a pending-request counter, where Node's net.Server waits for _connections to reach zero. A keep-alive connection that was mid-request when close() ran drops pending_requests to zero once that response is written, so the promise resolves while the TCP connection is still open and serving.

Two consequences of close() also nulling this[serverSymbol] immediately:

  • closeIdleConnections() / closeAllConnections() became no-ops after close(), so the standard close(); closeAllConnections() drain could not force the orphaned connection down.
  • closeAllConnections() itself called server.stop(true), which tears down the listener and fires 'close'; Node's contract is to destroy the connections and keep accepting.

Fix

All in _http_server.ts (no native change):

  • emitCloseServer is gated on kTrackedConnections.size === 0, mirroring Node's net.Server#_emitCloseIfDrained. When the native promise resolves with connections still tracked it records kPendingDrainClose and returns; the last connection's #onClose re-checks and schedules the actual 'close' emit.
  • close() keeps the native handle reachable under a kClosing flag instead of nulling serverSymbol (so address() reports null and a second close() reports ERR_SERVER_NOT_RUNNING, while the drain helpers still work).
  • closeAllConnections() iterates kTrackedConnections and destroy()s each socket, leaving the listener alone.
  • closeIdleConnections() does the native idle sweep (knows about partially-parsed requests) and additionally iterates kTrackedConnections, skipping sockets with an in-flight response, so it keeps working after close() once the native app has deinit'd.

Verification

New describe("server.close() drains connections, not requests") in test/js/node/http/node-http.test.ts covers the four graceful-shutdown shapes plus the listener invariant:

# without the fix                                       # with the fix
A: close() FINs an idle keep-alive connection     pass  pass  (control)
C: close(cb) waits while a keep-alive conn open   FAIL  pass
B: closeIdleConnections() after close() drains    FAIL  pass
B': closeAllConnections() after close() drains    FAIL  pass
D: cb never fires while client keeps conn busy    FAIL  pass  (servedAfterCb 4 -> 0)
closeAllConnections() leaves the listener running FAIL  pass

Also green: test-http-server-close-{all,idle,idle-wait-response,destroy-timeout}.js, test-https-server-close-{all,idle}.js, test-http-server-connection-list-when-close.js, test-http-req-res-close.js, bun-server.test.ts "late keep-alive request", node-http-server-timeouts.test.ts.

Two bun-parallel tests and five test cleanups that used closeAllConnections() as a stand-in for close() now call close() explicitly, matching Node.

Relationship to open PRs

#35837 adds the kTrackedConnections.size gate alone (scenario C); #35839 rewrites closeIdleConnections()/closeAllConnections() off the set (scenarios B/B' and the listener invariant, like #33394). #31302 and #30505 are earlier partial approaches to the closeAllConnections() half. This PR is the unified fix for the full drain contract and subsumes #35837, #35839, and #33394.


no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/web/fetch/fetch.stream.test.ts

Fixes #31301
Fixes #30501

server.close(cb) was wired to Bun.serve's all-closed promise, whose
condition is pending_requests == 0 && !listener && !websockets. A
keep-alive connection that was serving a request when close() ran is
still open once that request finishes, so the callback fired (and
'close' emitted) while the connection kept accepting requests.

close() also nulled this[serverSymbol] immediately, so
closeIdleConnections() and closeAllConnections() became no-ops after
close(), and closeAllConnections() was a full stop(true) that tore
down the listener.

Gate emitCloseServer on kTrackedConnections.size (re-checked from the
last connection's #onClose), keep the native handle reachable under a
kClosing flag until the server has actually drained, and rewrite
closeAllConnections()/closeIdleConnections() to iterate
kTrackedConnections so they work before and after close() without
touching the listener.
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

The HTTP server now keeps its native listener during server.close() connection draining, separates closeAllConnections() from listener shutdown, defers close emission until tracked sockets drain, and resets state for subsequent listen() calls. Tests cover drain semantics and explicit teardown.

HTTP server close and drain lifecycle

Layer / File(s) Summary
Close state and server APIs
src/js/node/_http_server.ts
Adds closing-state flags, defers close emission while connections remain, keeps the listener during draining, and updates closeAllConnections(), closeIdleConnections(), close(), and address().
Listen and socket lifecycle coordination
src/js/node/_http_server.ts
Suppresses listening during close, resets drain flags on relisten, and schedules final close processing after tracked sockets disappear.
Drain behavior and cleanup coverage
test/js/node/http/node-http.test.ts, test/js/bun/test/parallel/*, test/js/first_party/ws/ws.test.ts, test/js/node/http/node-http-with-ws.test.ts, test/js/web/fetch/*
Adds coverage for listener state, interval destruction, keep-alive draining, idle and all-connection closure, header parsing, WebSocket cleanup, and explicit server shutdown.

Possibly related PRs

  • oven-sh/bun#35839: Also changes HTTP server idle and all-connection shutdown behavior.
  • oven-sh/bun#35025: Also modifies NodeHTTPServerSocket close-path behavior.
  • oven-sh/bun#35063: Also changes NodeHTTPServerSocket.#onClose behavior in the HTTP server shutdown path.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: server.close now waits for connection drain instead of request count.
Description check ✅ Passed The description covers the PR behavior and verification, though it uses custom sections instead of the template headings.
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.

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

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. node:http: Server.closeAllConnections() shuts down the listening socket #31301 - PR directly fixes closeAllConnections() tearing down the listening socket instead of only destroying established connections

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

Fixes #31301

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. node:http: defer server 'close' until every tracked connection has ended #35837 - Explicitly subsumed; same fix to gate server.close() on connection drain in _http_server.ts
  2. node:http: make closeAllConnections()/closeIdleConnections() leave the listener alone and work after close() #35839 - Same fix area; makes closeIdleConnections/closeAllConnections work after close(), which this PR also implements
  3. http: closeAllConnections() must not stop the listener #33394 - Explicitly subsumed; fixes closeAllConnections() tearing down the listener
  4. fix(node:http): keep listener open in Server.closeAllConnections #31302 - Same fix as http: closeAllConnections() must not stop the listener #33394; keeps listener open in closeAllConnections()
  5. fix(http): preserve server reference across close() for closeAllConnections() #30505 - Fixes getTokenInteractive in @azure/msal-node will cause Bun to hang after the end of the script #30501 (same issue this PR fixes); preserves server reference for closeAllConnections() after close()

🤖 Generated with Claude Code

Comment thread src/js/node/_http_server.ts
Comment thread src/js/node/_http_server.ts
Comment thread src/js/node/_http_server.ts Outdated
…isten

The kTrackedConnections pass in closeIdleConnections() treated a
connection whose request head is still arriving as idle (no _httpMessage
yet). On a live server the native isIdle sweep already spares it, so
restrict the Set pass to the kClosing window it exists for.

Reset kClosing/kPendingDrainClose in kRealListen and bail in
emitCloseServer when !kClosing so a listen() during a prior close()'s
drain does not have its new handle cleared by the stale drain, and gate
emitListeningNextTick on !kClosing for the same _handle-stand-in
consistency as address().
Comment thread src/js/node/_http_server.ts Outdated
Comment thread src/js/node/_http_server.ts Outdated
Comment thread src/js/node/_http_server.ts Outdated
Comment thread src/js/node/_http_server.ts Outdated
Comment thread src/js/node/_http_server.ts Outdated
@robobun

robobun commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 10:18 PM PT - Jul 25th, 2026

@robobun, your commit 768edc0 has 1 failures in Build #82044 (All Failures):

  • 📦 Binary size — 12 over 0.50 MB
  • targetthis build canary: main #79916
    sizeΔ
    bun-darwin-aarch6458.13 MB57.58 MB+564.9 KB
    bun-darwin-x6463.48 MB62.95 MB+544.5 KB
    bun-linux-aarch6470.98 MB70.42 MB+576.0 KB
    bun-linux-x6472.47 MB71.95 MB+528.0 KB
    bun-linux-aarch64-musl64.88 MB64.32 MB+576.0 KB
    bun-linux-x64-musl66.98 MB66.45 MB+544.0 KB
    bun-linux-aarch64-android78.47 MB77.97 MB+512.0 KB
    bun-linux-x64-android80.62 MB80.10 MB+529.2 KB
    bun-freebsd-x6483.07 MB82.56 MB+528.0 KB
    bun-freebsd-aarch6484.84 MB84.31 MB+544.0 KB
    bun-windows-x6480.26 MB79.70 MB+570.5 KB
    bun-windows-aarch6470.86 MB70.34 MB+534.0 KB

    Add [skip size check] to the commit message if this increase is intentional.


🧪   To try this PR locally:

bunx bun-pr 35844

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

bun-35844 --bun

Comment thread src/js/node/_http_server.ts
Comment thread src/js/node/_http_server.ts
Comment thread src/js/node/_http_server.ts

@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: 2

🤖 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 `@src/js/node/_http_server.ts`:
- Around line 527-534: Update the kClosing idle-sweep loop in the HTTP server to
reuse the native idle-socket predicate, preserving sockets currently parsing
request headers even when _httpMessage is not yet attached. Keep the existing
checks and destruction behavior for truly idle sockets, and add a regression
test covering closeIdleConnections() after close() during mid-header parsing.

In `@test/js/node/http/node-http.test.ts`:
- Around line 3471-3475: Move the entire “server.close() drains connections, not
requests” suite and its supporting setup into a dedicated topic-specific HTTP
test file named node-http-close-drain.test.ts, removing it from
node-http.test.ts. Preserve all six tests and their existing behavior without
unrelated refactoring.
🪄 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: d12f1121-2c81-466a-b805-49f3961fe8ef

📥 Commits

Reviewing files that changed from the base of the PR and between 44f6469 and e0f88d5.

📒 Files selected for processing (8)
  • src/js/node/_http_server.ts
  • test/js/bun/test/parallel/test-http-server.listening-should-work.ts
  • test/js/bun/test/parallel/test-http-timeout-destruction-should-be-visible-using-kConnectionsCheckingInterval.ts
  • test/js/first_party/ws/ws.test.ts
  • test/js/node/http/node-http-with-ws.test.ts
  • test/js/node/http/node-http.test.ts
  • test/js/web/fetch/client-fetch.test.ts
  • test/js/web/fetch/fetch.stream.test.ts

Comment thread src/js/node/_http_server.ts
Comment thread test/js/node/http/node-http.test.ts

@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.

Additional findings (outside current diff — PR may have been updated during review):

  • 🔴 src/js/node/_http_server.ts:713-716 — The re-listen-during-drain reset at lines 715–716 clears kClosing and kPendingDrainClose but not kCloseCallback, and the new if (!self[kClosing]) return at line 133 skips callCloseCallback — so close(cb1); listen(); /* old drain resolves */ close(cb2) now throws Error: Close callback already set at setCloseCallback (line 154), where pre-PR it fired cb1 and cleared the slot, and Node stacks both via once('close', cb). Add this[kCloseCallback] = undefined alongside the other resets (or have the !kClosing early-return still run callCloseCallback).

    Extended reasoning...

    What the bug is

    Commit e0f88d5 (the follow-up in this PR that fixes the re-listen-during-drain handle leak flagged in the earlier review) added two things: an if (!self[kClosing]) return; guard at the top of emitCloseServer (line 133), and a drain-state reset block in kRealListen (lines 715–716) that clears this[kClosing] = false and this[kPendingDrainClose] = false. But the reset block does not clear this[kCloseCallback], and the new early-return in emitCloseServer bypasses callCloseCallback(self) at line 142 — the only place that clears kCloseCallback (src/js/internal/http.ts:174-179). The result is that a re-listen during a drain leaves the first close callback stuck in the slot, and the next server.close(cb) throws.

    Step-by-step proof

    1. server.listen(0)kRealListen sets serverSymbol = OLD, arms getBunServerAllClosedPromise(OLD).$then(emitCloseNTServer) (line ~1194).
    2. server.close(cb1) → line 550 sets this[kClosing] = true; line 551 setCloseCallback(this, cb1) sets this[kCloseCallback] = cb1; OLD.stop().
    3. server.listen(0) (before OLD's all-closed promise resolves) → kRealListen runs: line 715 this[kClosing] = false, line 716 this[kPendingDrainClose] = false, line 717 this[serverSymbol] = NEW, arms NEW's all-closed promise. this[kCloseCallback] is still cb1 — the reset block does not touch it.
    4. OLD's all-closed promise resolves → emitCloseNTServerprocess.nextTick(emitCloseServer, this). emitCloseServer line 133: if (!self[kClosing]) returnkClosing was reset to false in step 3, so it returns immediately without reaching callCloseCallback(self) at line 142. kCloseCallback still holds cb1.
    5. Later, server.close(cb2) → line 537 if (!server || this[kClosing])serverSymbol = NEW and kClosing = false, so it proceeds; line 550 sets kClosing = true; line 551 setCloseCallback(this, cb2). setCloseCallback (lines 149–157): cb2 !== self[kCloseCallback] (which is cb1), and self[kCloseCallback] is truthy, so line 154 throws new Error("Close callback already set").

    Why this is a regression introduced by this PR

    Before this PR, emitCloseServer was just callCloseCallback(self); self.emit("close") with no !kClosing guard, so step 4 would have unconditionally fired cb1 and cleared the slot (with a spurious 'close' emit), and step 5's setCloseCallback(this, cb2) would have found an empty slot and succeeded. Node uses this.once('close', cb) for the close callback, so both cb1 and cb2 stack as listeners and fire together when the re-listened server eventually closes — it never throws. The throw is new behavior introduced by e0f88d5, in exactly the close→re-listen-during-drain scenario that commit's own comment at lines 713–714 says it handles ("A listen() during a prior close()'s drain installs a fresh handle; the pending drain must not clear it").

    Why existing code doesn't prevent it

    callCloseCallback (src/js/internal/http.ts:174-179) is the only path that nulls kCloseCallback, and it is now reachable from emitCloseServer only when self[kClosing] is still true at the time the drain completes. kRealListen's reset block is the third piece of drain state alongside kClosing and kPendingDrainClose, and per REVIEW.md's "fix the whole class in the same PR" it belongs in the same reset — the previous review round's fix that added lines 715–716 was two-out-of-three.

    Impact

    A hard synchronous throw on a public node:http API in a sequence that worked before this PR and works in Node. The window this PR intentionally lengthens (drain now waits for connections, not requests — up to keepAliveTimeout) makes the re-listen-during-drain race correspondingly easier to hit than before.

    Fix

    One line: add this[kCloseCallback] = undefined; next to lines 715–716 in kRealListen, so the reset block clears all three pieces of drain state together. This silently drops cb1 — the least-surprising choice given the reset block's stated intent (cancel the pending drain), and closer to pre-PR behavior minus the spurious 'close'. The alternative — having the !kClosing early-return still call callCloseCallback(self) before returning — fires cb1 at re-listen time, which is closer to Node's eventual-fire semantics but conflates "drain cancelled" with "drain completed".

Comment thread test/js/node/http/node-http.test.ts
…inally blocks

The re-listen reset cleared kClosing/kPendingDrainClose but not
kCloseCallback, so close(cb1); listen(); close(cb2) hit
setCloseCallback's 'Close callback already set' throw.

The new drain tests' finally blocks only called closeAllConnections(),
which no longer stops the listener; add server.close() so a failure
before the happy-path close() does not leak the listener.
@robobun

robobun commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed both findings from the second review round in 0a908e4:

  • kRealListen now clears kCloseCallback alongside kClosing/kPendingDrainClose, so close(cb1); listen(); close(cb2) no longer hits setCloseCallback's throw (cb1 is dropped, matching the reset block's "cancel the pending drain" intent).
  • server.close() added to the finally blocks of tests A/C/B/B'/D so a failure before the happy-path close does not leak the listener.

Comment thread src/js/node/_http_server.ts Outdated
Comment thread src/js/node/_http_server.ts Outdated
Comment thread src/js/node/_http_server.ts Outdated
Clearing serverSymbol before the fallible Bun.serve() call orphaned a
live listener in the listen() -> listen(fails) path. Move the drain-state
reset to after the assignment so a throw leaves the server in exactly its
pre-listen() state.
Comment thread src/js/node/_http_server.ts

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/js/node/_http_server.ts (1)

293-293: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Bind queued listening emission to the current listen cycle.
kClosing alone does not block a stale tick after listen(cb1); close(); listen(cb2): the second listen() clears it before the first timer runs, so the old callback can emit listening on the new handle and fire the wrong once("listening") listener. Pass a generation token or the installed handle into emitListeningNextTick and require it to still match server[serverSymbol] before emitting.

🤖 Prompt for 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.

In `@src/js/node/_http_server.ts` at line 293, Update the listening emission flow
around emitListeningNextTick to bind each queued callback to the listen cycle
that created it, using a generation token or the installed handle. Before
setting listening or emitting the event, require the captured token/handle to
still match server[serverSymbol], while preserving normal emission for the
current listen cycle.
🤖 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.

Outside diff comments:
In `@src/js/node/_http_server.ts`:
- Line 293: Update the listening emission flow around emitListeningNextTick to
bind each queued callback to the listen cycle that created it, using a
generation token or the installed handle. Before setting listening or emitting
the event, require the captured token/handle to still match
server[serverSymbol], while preserving normal emission for the current listen
cycle.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 183ed8f0-c428-4b0e-bd6f-9e53647da0d9

📥 Commits

Reviewing files that changed from the base of the PR and between e0f88d5 and 768edc0.

📒 Files selected for processing (2)
  • src/js/node/_http_server.ts
  • test/js/node/http/node-http.test.ts

@robobun

robobun commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author

Re the outside-diff finding on emitListeningNextTick (stale 'listening' timer after listen(); close(); listen()): that's pre-existing behaviour this PR does not change. Before this PR emitListeningNextTick checked only !!self[serverSymbol], and in that sequence serverSymbol is the new handle by the time either timer fires, so the first timer emitted on the new handle then too. The && !self[kClosing] addition only covers the single-listen case where close() races the 1ms timer; the double-listen stale-emit is the domain of #35498 (ERR_SERVER_ALREADY_LISTEN on second listen()), which is the proper fix for that path. Leaving it out of scope here.

@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 new issues found after 768edc0. Deferring to a maintainer given the scope: this reworks the node:http server close/drain state machine (kClosing/kPendingDrainClose, deferred serverSymbol clear, re-listen reset) and changes closeAllConnections() from stop(true) to a socket-set sweep — broad Node-compat surface that's worth a human sign-off.

What was reviewed:

  • The final kRealListen shape (768edc0): resets committed only after Bun.serve() succeeds, so both close()→listen(fails) and listen()→listen(fails) leave state intact.
  • emitCloseServer's kTrackedConnections gate + #onClose reschedule; !kClosing bail correctly suppresses the OLD promise after a successful re-listen.
  • closeIdleConnections() Set fallback is gated on kClosing so the native mid-parse predicate stays authoritative on live servers.
  • Test cleanups: all seven new tests plus the five external call sites now server.close() in finally.
Extended reasoning...

Overview

The PR changes src/js/node/_http_server.ts to gate server.close(cb)'s 'close' emit on kTrackedConnections.size === 0 (mirroring Node's net.Server#_emitCloseIfDrained) instead of the native pending_requests == 0 signal alone. To make that work it introduces a kClosing/kPendingDrainClose state pair, defers clearing serverSymbol until emitCloseServer, rewrites closeAllConnections() to iterate the tracked-socket Set (leaving the listener alone), extends closeIdleConnections() with a post-close Set fallback, and adds a re-listen reset in kRealListen. Seven new tests in node-http.test.ts cover the drain contract; five other test files add server.close() after closeAllConnections() since the latter no longer stops the listener.

Security risks

None identified. This is server-lifecycle bookkeeping in the JS compat layer; no auth, crypto, or untrusted-input parsing is touched.

Level of scrutiny

High. node:http server close/drain is a hot compat path (graceful shutdown, k8s SIGTERM handlers, test frameworks). The PR went through four prior review rounds here, each surfacing a real edge (test cleanup leak, stale kCloseCallback on re-listen, stale serverSymbol on failed re-listen, orphaned OLD handle on listen()→listen(fails)) — all now addressed, but the density of state-machine edges argues for a maintainer confirming the final shape and the closeAllConnections() semantic change (which, while Node-correct, is a behavioral change for anyone using it as a stand-in for close()).

Other factors

The PR subsumes three open PRs (#35837, #35839, #33394) and fixes #31301/#30501, so a maintainer should also decide the disposition of those. All prior inline findings on this PR are resolved; the bug-hunting pass on 768edc0 found nothing. I traced the two re-listen failure shapes against the final code and both leave the server in its pre-listen() state as claimed.

@robobun

robobun commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author

CI on 768edc0 (#82044): 171 lanes passed and no file this PR touches appears in any failure annotation. The seven new drain tests and all test-http*-server-close-*.js node-parallel tests are green on every lane that ran.

Remaining red is unrelated to this diff:

  • binary-size is comparing against the stale canary baseline #79916 and is also failing on main (#81770, #81444).
  • linux x64-asan build-bun timed out, which cascaded to the 20 debian x64-asan test-bun lanes never running; one darwin 14 aarch64 test-bun lane expired. All CI infra, not test failures.
  • The nine annotated test failures (bun-create, bun-upgrade, fastutf8stream-reopen, next-pages dev-server, no-orphans, 20144, fetch-connection-header, http2-server-push-stream, fetch-leak) are all flaky and passed on retry.

Ready for maintainer review.

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Closing to consolidate; the two things this PR does are now split across two open PRs:

  • closeAllConnections() / closeIdleConnections() (scenarios B, B' and the listener invariant): node:http: make closeAllConnections()/closeIdleConnections() leave the listener alone and work after close() #35839, which has been brought up to date with main. It uses the same tracked-set loop for both methods, and additionally skips upgraded sockets (parser == null, as Node's parser-keyed ConnectionsList does) and connections that are mid-request-head, which the native-sweep-when-live / set-sweep-when-closing split here did not cover uniformly. It also avoids keeping the native handle alive past close() behind a kClosing flag, so none of the other serverSymbol users need auditing.
  • Deferring the server 'close' event until every tracked connection has ended (scenarios C and D): that is a separate compat gap and is carried on its own by node:http: defer server 'close' until every tracked connection has ended #35837, which still applies cleanly to main. Scenario D from here would be a good addition to its tests.

All of these scenarios were re-tested against a fresh main build before closing: none of them is fixed on main yet (#35130 only gated Bun.serve's drain promise and deliberately left node:http out), so the fixes continue in those two PRs.

@robobun robobun closed this Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants