Skip to content

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

Open
robobun wants to merge 3 commits intomainfrom
farm/577dccf9/fix-socket-destroy-on-native-close
Open

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

Conversation

@robobun
Copy link
Copy Markdown
Collaborator

@robobun robobun commented Mar 31, 2026

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)

Copy link
Copy Markdown
Contributor

@claude claude bot left a comment

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
Copy link
Copy Markdown
Collaborator Author

robobun commented Mar 31, 2026

Updated 10:41 AM PT - Apr 1st, 2026

@autofix-ci[bot], your commit 57c675f has 6 failures in Build #43109 (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
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 31, 2026

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
Copy link
Copy Markdown

bilby91 commented Apr 1, 2026

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

@bilby91
Copy link
Copy Markdown

bilby91 commented Apr 1, 2026

@robobun Can you try again ?

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 4 times, most recently from a66bd4e to 3f6f199 Compare April 1, 2026 15:28
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

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 a66bd4e and 3f6f199.

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

@robobun robobun force-pushed the farm/577dccf9/fix-socket-destroy-on-native-close branch from 3f6f199 to d64d8b2 Compare April 1, 2026 15:55
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

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

📒 Files selected for processing (1)
  • 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
Copy link
Copy Markdown

bilby91 commented Apr 1, 2026

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

#28733

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