Skip to content

server: close TCP listener when HTTP/3 UDP bind fails - #32542

Closed
robobun wants to merge 2 commits into
mainfrom
farm/332ffdac/http3-listen-fail-close-tcp
Closed

robobun wants to merge 2 commits into
mainfrom
farm/332ffdac/http3-listen-fail-close-tcp

Conversation

@robobun

@robobun robobun commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

When Bun.serve with http3: true successfully binds the TCP port but the QUIC listener cannot bind the matching UDP port on the final attempt, the error path threw and called deinit() while the TCP listen socket was still linked into the uWS socket group. ~TemplatedApp then hit the head_listen_sockets == NULL assertion in debug builds; in release builds the listen socket kept polling into a freed HttpContext and segfaulted on the next accept.

What changed

Close the TCP listener unconditionally when the H3 bind fails, then either retry (port: 0 with attempts remaining) or throw. Previously the close only ran on the retry branch.

How to reproduce

const dgram = require('dgram');
const sock = dgram.createSocket('udp4');
sock.bind(0, '127.0.0.1', () => {
  const port = sock.address().port;
  Bun.serve({ port, hostname: '127.0.0.1', tls, http3: true, fetch: () => new Response('ok') });
});

Before: debug assertion group->head_listen_sockets == NULL failed; release build segfaults if anything connects to the leaked TCP port.
After: throws Failed to listen on UDP port N for HTTP/3 and the TCP port is released.

Found by Fuzzilli (fingerprint 23068b3612b07851).

When Bun.serve with http3: true successfully binds the TCP port but the
QUIC listener cannot bind the matching UDP port on the final attempt,
the error path threw and called deinit() while the TCP listen socket was
still linked into the uWS socket group. ~TemplatedApp then hit the
head_listen_sockets == NULL assertion in debug builds, and in release
builds left the listen socket polling into a freed HttpContext, crashing
on the next accept.

Close the TCP listener unconditionally when the H3 bind fails, then
either retry (port: 0 with attempts remaining) or throw.
@robobun

robobun commented Jun 20, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 10:52 AM PT - Jun 20th, 2026

❌ @robobun, your commit 16f916e has 1 failures in Build #63658 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 32542

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

bun-32542 --bun

@coderabbitai

coderabbitai Bot commented Jun 20, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@robobun, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 4 minutes and 44 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b5791b46-2cae-47a0-b299-10c078430295

📥 Commits

Reviewing files that changed from the base of the PR and between ad90561 and 16f916e.

📒 Files selected for processing (1)
  • test/js/bun/http/serve-http3.test.ts

Walkthrough

In NewServer::listen, the H3 retry loop now unconditionally closes the TCP listener when the H3 UDP bind fails, instead of only doing so on non-final attempts. A new regression test verifies that after Bun.serve throws on a UDP-occupied port, no TCP listener remains open.

Changes

H3 UDP Bind Failure: TCP Listener Cleanup

Layer / File(s) Summary
TCP listener cleanup on H3 bind failure + regression test
src/runtime/server/mod.rs, test/js/bun/http/serve-http3.test.ts
When h3_listener is None after a UDP bind attempt, self.listener.take() and close() are now called unconditionally before the retry/throw decision. The new test binds a UDP socket to force the failure, asserts Bun.serve throws an "HTTP/3" error, and then confirms the TCP port is not left open by checking that a subsequent TCP connect returns CLOSED rather than LEAKED.

Possibly related PRs

  • oven-sh/bun#30741: Touches NewServer::listen H3 retry logic in the same src/runtime/server/mod.rs file, changing config.h3 to config.http3 gating and related retry conditions—directly adjacent to the cleanup path modified here.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main fix: closing the TCP listener when HTTP/3 UDP bind fails, which directly addresses the core issue in the changeset.
Description check ✅ Passed The description includes both required template sections: 'What changed' provides context, and 'How to reproduce' demonstrates verification, though the template asks for 'How did you verify'.
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.

✏️ 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.

@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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/js/bun/http/serve-http3.test.ts`:
- Around line 1107-1108: The error assertion on line 1108 uses a broad substring
check for "HTTP/3" which could be satisfied by any early HTTP/3 setup error
instead of specifically testing the UDP bind failure for the port. Replace the
substring check with an assertion that matches the exact error message thrown by
the listen() function when UDP binding fails on the specific port being tested.
This ensures the test actually exercises the intended TCP-success/UDP-failure
code path rather than passing vacuously on unrelated HTTP/3 errors.
🪄 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: f85d20bb-b7f1-4d0c-9f53-631bf900f618

📥 Commits

Reviewing files that changed from the base of the PR and between 8841747 and ad90561.

📒 Files selected for processing (2)
  • src/runtime/server/mod.rs
  • test/js/bun/http/serve-http3.test.ts

Comment thread test/js/bun/http/serve-http3.test.ts Resolved
@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. server: close TCP listener when H3 UDP bind fails #32539 - Fixes the exact same bug (TCP listener not closed when H3 UDP bind fails) with the same code change hoisting listener.take().close() outside the retry check
  2. Bun.serve: close listen socket before destroying app in deinit() #32543 - Fixes the same root cause (listen socket leak on H3 UDP bind failure) but from the deinit() side, closing the listener before destroying the uws app

🤖 Generated with Claude Code

@robobun

robobun commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator Author

Closing as a duplicate of #32539, which has the identical source change and was opened first.

@robobun robobun closed this Jun 20, 2026
@robobun
robobun deleted the farm/332ffdac/http3-listen-fail-close-tcp branch June 20, 2026 17:51
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.

1 participant