Skip to content

Bun.serve: print the dev-mode idle timeout warning once for all protocols - #43017

Open
robobun wants to merge 3 commits into
mainfrom
robobun/efdc6321/serve-idle-warn-once
Open

robobun wants to merge 3 commits into
mainfrom
robobun/efdc6321/serve-idle-warn-once

Conversation

@robobun

@robobun robobun commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • In development mode, Bun.serve prints the idle timeout warning (warn: Bun.serve() timed out a request after 10 seconds. Pass idleTimeout to configure.) two times when one HTTP/1 request and one HTTP/2 or HTTP/3 request time out in the same process. The second copy reads [Bun.serve]: request timed out after 10 seconds.
  • The HTTP/1 path (NewServer::on_timeout_for_idle_warn, src/runtime/server/mod.rs:661) and the HTTP/2 and HTTP/3 path (on_timeout_for_idle_warn, src/runtime/server/server_body.rs:1218) each had their own once-only flag and their own text. should_add_timeout_handler_for_warning read only the HTTP/1 flag.

Fix

  • Remove the HTTP/1 copy of the handler and the flag from mod.rs. The HTTP/1 request path now registers the shared handler from server_body.rs, the same one the RespLike::on_timeout_warn closures use for HTTP/2 and HTTP/3.
  • should_add_timeout_handler_for_warning reads the shared flag. After the warning prints, no later request on any protocol registers the handler again.
  • The shared handler prints the warn: text, the text the HTTP/1 path printed before, and flushes stderr. This matches the text that the regression test in Output: flush stderr after a warning #43004 expects.
  • Verified: test/regression/issue/43016.test.ts (fails on main with two warnings, passes with the fix). Also test/js/bun/http/serve-http2.test.ts and the idle timeout tests in serve.test.ts.

Background

  • The warning exists so a developer learns that the default 10 second idleTimeout closed a request. It is meant to print at most once per process, for all servers and all protocols.
  • uWS calls the on_timeout callback of a response when the socket idle timer fires. The server registers the warning callback only while the flag is false and the user did not pass idleTimeout.
  • Before this change, the Zig server had one handler for all protocols. The Rust port split it into two.

Fixes #43016


[human-review] gate passed · iteration 0 · 3 files touched

fails on main (without fix)
ASAN without fix: 1 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" "test/regression/issue/43016.test.ts"
bun test v1.4.3 (c6b7fcb5b)

test/regression/issue/43016.test.ts:
66 | 
67 |   const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
68 |   expect(stdout).toBe("both closed, requests seen: h1,h2\n");
69 |   const warning = "warn: Bun.serve() timed out a request after 10 seconds. Pass `idleTimeout` to configure.";
70 |   expect(stderr.split(warning).length - 1).toBe(1);
71 |   expect(stderr).not.toContain("[Bun.serve]: request timed out");
                          ^
error: expect(received).not.toContain(expected)

Expected to not contain: "[Bun.serve]: request timed out"
Received: "[Bun.serve]: request timed out after 10 seconds. Pass `idleTimeout` to configure.\nwarn: Bun.serve() timed out a request after 10 seconds. Pass `idleTimeout` to configure.\n"

      at <anonymous> (/workspace/bun/test/regression/issue/43016.test.ts:71:22)
(fail) Bun.serve prints the idle timeout warning once for HTTP/1 and HTTP/2 timeouts [14417.83ms]

 0 pass
 1 fail
... (truncated)

release without fix: 1 FAILED
bun test v1.4.3-canary.1 (c6b7fcb5b)

test/regression/issue/43016.test.ts:
66 | 
67 |   const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
68 |   expect(stdout).toBe("both closed, requests seen: h1,h2\n");
69 |   const warning = "warn: Bun.serve() timed out a request after 10 seconds. Pass `idleTimeout` to configure.";
70 |   expect(stderr.split(warning).length - 1).toBe(1);
71 |   expect(stderr).not.toContain("[Bun.serve]: request timed out");
                          ^
error: expect(received).not.toContain(expected)

Expected to not contain: "[Bun.serve]: request timed out"
Received: "[Bun.serve]: request timed out after 10 seconds. Pass `idleTimeout` to configure.\nwarn: Bun.serve() timed out a request after 10 seconds. Pass `idleTimeout` to configure.\n"

      at <anonymous> (/workspace/bun/test/regression/issue/43016.test.ts:71:22)
(fail) Bun.serve prints the idle timeout warning once for HTTP/1 and HTTP/2 timeouts [12040.31ms]

 0 pass
 1 fail
 3 expect() calls
Ran 1 test across 1 file. [12.14s]
__F:1:S:0
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" "test/regression/issue/43016.test.ts"
bun test v1.4.3 (c6b7fcb5b)

test/regression/issue/43016.test.ts:
(pass) Bun.serve prints the idle timeout warning once for HTTP/1 and HTTP/2 timeouts [14013.99ms]

 1 pass
 0 fail
 4 expect() calls
Ran 1 test across 1 file. [15.90s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 663ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/7] gen generated_host_exports.rs
generated_host_exports.rs: 121 exports (host=5, lazy=10, generic=106, rust=0); 242 extern-C blocks audited
[1/7] cargo bun_runtime → libbun_runtime.a
�[1m�[33mwarning�[0m�[1m: binary `bun_shim_impl` should have a kebab-case name�[0m
   �[1m�[94m|�[0m
�[1m�[94m 1�[0m �[1m�[94m|�[0m /workspace/bun/build/release/rust-target/.../bun_shim_impl
   �[1m�[94m|�[0m                                              �[1m�[33m^^^^^^^^^^^^^�[0m
   �[1m�[94m|�[0m
   �[1m�[94m= �[0m�[1mnote�[0m: `cargo::non_kebab_case_bins` is set to `warn` by default
�[1m�[96mhelp�[0m: to change the binary name to `bun-shim-impl`, convert `bin.name`
  �[1m�[94m--> �[0msrc/install/windows-shim/Cargo.toml:41:8
   �[1m�[94m|�[0m
�[1m�[94m41�[0m �[91m- �[0mname = �[91m"bun_shim_impl"�[0m
�[1m�[94m41�[0m �[92m+ �[0mname = �[92m"bun-shim-impl"�[0m
   �[1m�[94m|�[0m
�[1m�[33mwarning�[0m: `bun_shim_impl` (manifest) generated 1 warning
�[1m�[33mwarning�[0m�[1m: `feature(generic_const_exprs)` is not supported with
... (truncated)
diff hotspot
src/runtime/server/mod.rs           | 30 +++------------
 src/runtime/server/server_body.rs   | 16 +++-----
 test/regression/issue/43016.test.ts | 73 +++++++++++++++++++++++++++++++++++++
 3 files changed, 84 insertions(+), 35 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                 reads  edits  tests
src/runtime/server/mod.rs                2      2      5
src/runtime/server/server_body.rs        2      4      5
test/regression/issue/43016.test.ts      0      6      5

root cause · written by the author bot

The Rust port of Bun.serve kept two copies of the dev-mode idle timeout warning handler, one for HTTP/1 in NewServer and one for HTTP/2 and HTTP/3 in server_body, each with its own once-only flag and its own message text, so a process that timed out a request on each path printed the warning twice and the registration check only consulted the HTTP/1 flag. The fix removes the HTTP/1 copy and routes all three protocols through the single server_body::on_timeout_for_idle_warn handler and its shared process-global AtomicBool, with the message emitted once via bun_core::warn!. A regr…

…cols

The HTTP/1 path and the HTTP/2 and HTTP/3 path each had their own
once-only flag and their own warning text. A process that timed out
one request on each protocol printed the warning two times.

Keep the one handler and flag in server_body.rs and use it from the
HTTP/1 request path as well. should_add_timeout_handler_for_warning
reads the same flag, so a later request does not register the handler
again after the warning printed.

Fixes #43016
@coderabbitai

coderabbitai Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 23851595-77be-453e-a467-ffbd729632d6

📥 Commits

Reviewing files that changed from the base of the PR and between 630e921 and 845950e.

📒 Files selected for processing (3)
  • src/runtime/server/mod.rs
  • src/runtime/server/server_body.rs
  • test/regression/issue/43016.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 2 remain after this review.


Walkthrough

The change moves idle-timeout warning state and callback handling into server_body, uses one shared warning latch for server timeout registration, emits the canonical warning through bun_core::warn!, and adds a regression test for HTTP/1 and HTTP/2 requests.

Changes

Idle-timeout warning handling

Layer / File(s) Summary
Shared warning state and emission
src/runtime/server/server_body.rs
The warning flag and callback are available to the parent module. Warning output uses bun_core::warn! with the plain timeout message while once-only and silent-mode checks remain.
Timeout registration and regression coverage
src/runtime/server/mod.rs, test/regression/issue/43016.test.ts
NewServer reads and passes the shared warning state and callback. The regression test verifies one warning after HTTP/1 and HTTP/2 idle timeouts.

Suggested reviewers: jarred-sumner, dylan-conway

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 84595

The timeout warning is emitted once across HTTP/1, HTTP/2, and HTTP/3, with no identified merge-blocking risk.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Issue #43016 requires one development-mode idle-timeout warning across HTTP/1, HTTP/2, and HTTP/3. The PR moves the warning state and callback to server_body.rs, makes the shared helpers available t…
Out of Scope Changes check ✅ Passed The reviewed changes are limited to the idle-timeout warning callback and shared once-only state in src/runtime/server/mod.rs and src/runtime/server/server_body.rs, plus the regression test for is…
Title check ✅ Passed The title clearly summarizes the main change: Bun.serve now emits the development-mode idle-timeout warning once across protocols.
Description check ✅ Passed The description explains the problem, fix, expected behavior, linked issue, and verification results. It does not use the template headings exactly, but it provides the required information in equival…

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

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline findings, I also checked the swap(true)-before-silent ordering the HTTP/1 path now inherits from the shared handler: both registration sites (should_add_timeout_handler_for_warning in mod.rs and the gate at server_body.rs:2950) already skip registering the callback when --silent is set, and silent is a process-wide CLI flag, so the handler can never run in a silent process and the latch-without-print case is unreachable.

Extended reasoning...

The Rust side of this diff is a small consolidation: two once-latches and two handler bodies become one, with the HTTP/1 path calling the shared server_body::on_timeout_for_idle_warn through a non-capturing closure shim. I traced the one semantic difference between the deleted and surviving handler (latching before the silent check) and confirmed it is unreachable because every registration site is gated on !silent, which cannot change during the process. The remaining concerns are the test-quality issues already posted inline (the HTTP/2 leg can pass without the stream reaching the server; the 10s wall-clock wait plus 30s per-test timeout), so a human still needs to weigh those before merge and I am not approving.

2 verified lower-impact observations (convention, logging or cleanup points) were not posted.

Comment thread test/regression/issue/43016.test.ts
Comment thread test/regression/issue/43016.test.ts
Comment thread src/runtime/server/server_body.rs Outdated
Comment thread src/runtime/server/server_body.rs Outdated
@robobun

robobun commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 1:10 AM PT - Sep 17th, 2026

✅ @robobun, your commit 352926d7d00e5dd89a6f490b8cdf6351e26c1bc5 passed in Build #116903! 🎉


🧪   To try this PR locally:

bunx bun-pr 43017

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

bun-43017 --bun

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

The follow-up commits address the point I raised about the HTTP/2 leg (the child now prints requests seen: h1,h2, so a client that fails before reaching the handler can no longer pass), and I found no bugs in the Rust change. One thing still needs a maintainer call rather than a bot: the test inherently waits out the default 10 s idle timer on two connections and sets a 30 s per-test ceiling, which the repo's test guidance treats as an exception to sign off on explicitly.

What was reviewed:

  • The HTTP/1 closure in mod.rs is byte-for-byte the shape already used by RespLike::on_timeout_warn for NewAppResponse in server_body.rs, and should_add_timeout_handler_for_warning still gates on DEBUG && !silent && !has_idle_timeout, so dropping the DEBUG check inside the shared handler is safe.
  • Grepped for assertions on the old [Bun.serve]: request timed out text — none remain outside the new test.
  • Latching the flag with swap before the silent check is pre-existing behavior of the shared handler and unreachable when silent, since the handler is never registered then.
Extended reasoning...

Overview

The PR removes the HTTP/1-only copy of the dev-mode idle-timeout warning (NewServer::on_timeout_for_idle_warn and its per-monomorphization AtomicBool) from src/runtime/server/mod.rs and routes the HTTP/1 request path through the shared server_body::on_timeout_for_idle_warn / did_send_idletimeout_warning_once that HTTP/2 and HTTP/3 already used. The shared handler's text switches from the [Bun.serve]: pretty_errorln! form to bun_core::warn!("Bun.serve() timed out a request after 10 seconds. ...") followed by Output::flush(). A regression test at test/regression/issue/43016.test.ts spawns one process with development: true, http2: true, stalls one HTTP/1 POST and one h2c POST stream, and asserts exactly one warn: line and no [Bun.serve]: line. Since my prior review, commits 58cf4ea and 352926d changed only doc comments and the test.

Security risks

None. The change touches a once-per-process diagnostic latch and its message text. The user-data pointer passed to uWS is an opaque sentinel that the handler never dereferences, exactly as before. No user input is parsed, no allocation or lifetime changes.

Level of scrutiny

Low for the native change: the new closure in mod.rs (|_: *mut c_void, _: &mut uws_sys::NewAppResponse<SSL>| server_body::on_timeout_for_idle_warn()) is the same non-capturing shape already compiled at server_body.rs:370, and should_add_timeout_handler_for_warning() continues to require DEBUG, !silent, and !has_idle_timeout before registering it, so the handler losing its own DEBUG check does not widen when the warning fires. The swap(true, Relaxed) ordering is a single-flag latch with no data it publishes; Relaxed is fine. I grepped the tree for the old message text and found no remaining assertions to update. No CODEOWNERS entry covers the changed paths.

Other factors

My earlier inline comment about the HTTP/2 leg being unable to fail was addressed by a real commit (headers x-proto: h1/h2 are recorded in the handler and echoed on stdout, asserted with toBe). The remaining item is a judgment call, not a defect: the warning is only registered when idleTimeout is absent, so the test cannot be made faster than the hard-coded 10 s default and needs the 30 s per-test ceiling. test/CLAUDE.md says not to set timeouts and the root guidance allows it only for a rare outlier, so a maintainer should decide whether this belongs in the regression lane as written or be restructured. I could not run the test here because no debug build is present and building is not appropriate within a review run, so I am relying on the PR's claim that it fails on main and passes with the fix.

This branch has not been deployed

No deployments
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.

Bun.serve prints the dev-mode idle timeout warning twice when HTTP/1 and HTTP/2 requests time out

1 participant