Skip to content

fix(net): destroy socket on native close to prevent server.close() hang#28732

Closed
robobun wants to merge 3 commits into
mainfrom
farm/577dccf9/fix-socket-destroy-on-native-close
Closed

fix(net): destroy socket on native close to prevent server.close() hang#28732
robobun wants to merge 3 commits into
mainfrom
farm/577dccf9/fix-socket-destroy-on-native-close

Conversation

@robobun

@robobun robobun commented Mar 31, 2026

Copy link
Copy Markdown
Collaborator

Problem

When a native socket closes, Bun's close handlers (SocketHandlers, ServerHandlers, SocketHandlers2) push null to the readable stream but never call destroy(). If the readable side is paused or the writable was never ended, autoDestroy never fires. The socket stays in a zombie state (_handle=null, destroyed=false), server._connections never decrements, and server.close() hangs forever.

Fixes #28731, #13184, #19563, #23648

Root Cause

In src/js/node/net.ts, all three close handlers rely on autoDestroy to eventually call _destroy(), which decrements server._connections and emits the close event. But autoDestroy only fires after both the readable side ends (endEmitted) and the writable side finishes. When the readable is paused or data hasn't been consumed, the readable end event never emits, so autoDestroy never triggers.

Fix

Add if (!self.destroyed) process.nextTick(destroyNT, self) at the end of each close handler. destroyNT already exists (line 94) and simply calls self.destroy(err). Using process.nextTick ensures the current close handler finishes before destruction runs, matching Node.js behavior.

Verification

  • USE_SYSTEM_BUN=1 bun test test/regression/issue/13184.test.ts: 2 pass, 6 fail (timeouts)
  • bun bd test test/regression/issue/13184.test.ts: 8 pass, 0 fail
  • Existing test/js/node/net/ tests: no new failures (all failures pre-existing)

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

⚠️ Code review skipped — your organization's overage spend limit has been reached.

Code review is billed via overage credits. To resume reviews, an organization admin can raise the monthly limit at claude.ai/admin-settings/claude-code.

Once credits are available, push a new commit or reopen this pull request to trigger a review.

@robobun

robobun commented Mar 31, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 5:50 PM PT - Apr 3rd, 2026

@autofix-ci[bot], your commit 5d8c41e has 2 failures in Build #43512 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 28732

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

bun-28732 --bun

@coderabbitai

coderabbitai Bot commented Mar 31, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Schedule deferred destruction via process.nextTick for net socket wrappers in several close handlers when the wrapper is not already destroyed; add a regression test suite ensuring net.Server.close() completes and sockets become destroyed === true in native-close and paused-readable scenarios.

Changes

Cohort / File(s) Summary
Socket lifecycle changes
src/js/node/net.ts
Insert process.nextTick(destroyNT, ...) calls in multiple socket close handlers (client-side SocketHandlers.close, server per-connection ServerHandlers.close, and non‑TLS SocketHandlers2.close) guarded by if (!...destroyed). Remove an obsolete // TODO comment.
Regression tests
test/regression/issue/13184.test.ts
Add new test file with testServerCloseCompletes helper and multiple tests asserting net.Server.close() does not hang and server-side sockets transition to destroyed === true across paused-readable, pipe/unpipe/transform, never-read, and both destroy/end teardown scenarios.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: adding socket destruction on native close to prevent server.close() hangs.
Description check ✅ Passed The description is comprehensive, covering the problem, root cause, fix, and verification with concrete test results.
Linked Issues check ✅ Passed Code changes correctly address the linked issue #28731 by calling destroy() on native socket close through process.nextTick, ensuring destroyed flag is set and server._connections decrements properly.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing the socket destruction issue: modifications to net.ts close handlers and addition of comprehensive regression test.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

@bilby91

bilby91 commented Apr 1, 2026

Copy link
Copy Markdown

@robobun The darwin failures timeouts are not related to this change and seem to be happening on recent prs as well.

@bilby91

bilby91 commented Apr 1, 2026

Copy link
Copy Markdown

@robobun Can you try again ?

@robobun robobun force-pushed the farm/577dccf9/fix-socket-destroy-on-native-close branch 4 times, most recently from a66bd4e to 3f6f199 Compare April 1, 2026 15:28

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/regression/issue/13184.test.ts`:
- Around line 9-13: Remove the inline explanatory paragraph in the test body of
issue/13184.test.ts (the lines starting "When the native socket closes, ..."
through the explanatory sentences) and leave only the single issue URL/reference
line(s) that point to the bug; i.e., trim the prose while keeping the minimal
issue reference so the test matches the repo regression-test style and contains
no extra context comments.
🪄 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: c6554c16-1575-42b6-86bb-8dc0194acea4

📥 Commits

Reviewing files that changed from the base of the PR and between a66bd4eb272899976a3917efb1e3fdb3b50af759 and 3f6f199.

📒 Files selected for processing (1)
  • test/regression/issue/13184.test.ts

Comment thread test/regression/issue/13184.test.ts Outdated
@robobun robobun force-pushed the farm/577dccf9/fix-socket-destroy-on-native-close branch from 3f6f199 to d64d8b2 Compare April 1, 2026 15:55

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/regression/issue/13184.test.ts`:
- Around line 16-20: The test currently verifies socket destruction only
indirectly; add an explicit assertion using the Jest expect helper to validate
socket.destroyed after handler runs: import expect (or ensure the testing
framework's expect is available) at the top of the file and inside the
net.createServer callback add an assertion such as
expect(socket.destroyed).toBeTruthy() (or toBe(true)) alongside the existing
socket.on("close", onServerSocketClose) to fail fast and give clearer
diagnostics; update references in the callback where socket, handler,
onServerReady, and onServerSocketClose are used.
🪄 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: 9c4f3b98-2ea0-44bd-8cc9-7bea5bd6047e

📥 Commits

Reviewing files that changed from the base of the PR and between 3f6f199 and d64d8b282b35af532468a672887e2f7d6f61a4bf.

📒 Files selected for processing (1)
  • test/regression/issue/13184.test.ts

Comment thread test/regression/issue/13184.test.ts
@robobun robobun force-pushed the farm/577dccf9/fix-socket-destroy-on-native-close branch from d64d8b2 to 2290256 Compare April 1, 2026 16:21
@bilby91

bilby91 commented Apr 1, 2026

Copy link
Copy Markdown

@robobun The last PR that was merged also has the webview timeout issues that we are seeing in darwin.

#28733

@bilby91

bilby91 commented Apr 3, 2026

Copy link
Copy Markdown

@robobun Can you try again from main? Tests were deflaked. We might have luck this time.

robobun and others added 3 commits April 3, 2026 23:05
When a native socket closes, the close handlers push null to the readable
stream but never call destroy(). If the readable side is paused or the
writable was never ended, autoDestroy never fires. The socket stays in a
zombie state (_handle=null, destroyed=false), server._connections never
decrements, and server.close() hangs forever.

Add process.nextTick(destroyNT, self) to all three close handlers
(SocketHandlers, ServerHandlers, SocketHandlers2) so the socket always
transitions to destroyed=true after native close, matching Node.js behavior.
@robobun robobun force-pushed the farm/577dccf9/fix-socket-destroy-on-native-close branch from 57c675f to 5d8c41e Compare April 3, 2026 23:05

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

This fix touches the core socket lifecycle ( → decrement) for all TCP connections, so it warrants a human sign-off before merging.

Extended reasoning...

Overview

The PR adds three guarded calls — one each in , , and in . It also adds a regression test suite () covering paused-readable, pipe/unpipe, and graceful-end teardown scenarios.

Security risks

None — this is a socket lifecycle correctness fix with no auth, crypto, or permission changes.

Level of scrutiny

The change is small and mechanical (identical 1-line pattern in three handlers, using an already-existing helper), but it modifies the fundamental socket destruction path that affects every TCP connection in Bun. The method decrements and gates completion, so an incorrect guard or double-destroy regression could break all server teardown. The PR includes solid test coverage and a clear root cause analysis, but the blast radius justifies a human reviewer confirming the fix.

Other factors

All coderabbitai review comments have been addressed (prose removal, socket.destroyed assertion discussion resolved). The build is in progress. No bugs were found by the automated system. The fix correctly mirrors Node.js behavior by using rather than a synchronous call.

@robobun

robobun commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #31654, which is a strict superset of this change: it keeps the process.nextTick(destroyNT, ...) rescue for the paused/unpiped readable (passing this PR's regression/issue/13184.test.ts scenarios) and additionally handles the write-backpressure variant and preserves correct error semantics — a peer RST surfaces as 'error' + 'close'(hadError=true) instead of being swallowed as 'end'. Suggest consolidating there; closing this to avoid competing copies.

@robobun robobun closed this Jun 1, 2026
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.

server.close() hangs when sockets are closed natively while readable is paused

2 participants