Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 5 additions & 25 deletions src/runtime/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -647,31 +647,9 @@ pub(crate) fn wrap_handler_slot(
}

impl<const SSL: bool, const DEBUG: bool> NewServer<SSL, DEBUG> {
/// Per-monomorphization static.
/// Rust statics cannot be const-generic; routed through a
/// `&'static AtomicBool` so the four (SSL,DEBUG) instantiations share one
/// flag — the warning is process-global by intent (printed at most once
/// regardless of how many servers are running).
fn did_send_idletimeout_warning_once() -> &'static core::sync::atomic::AtomicBool {
static FLAG: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false);
&FLAG
}

/// The body ignores both arguments so a single non-generic shim suffices.
fn on_timeout_for_idle_warn(_: *mut c_void, _: &mut uws_sys::NewAppResponse<SSL>) {
if DEBUG && !Self::did_send_idletimeout_warning_once().load(Ordering::Relaxed) {
if !crate::cli::Command::get().debug.silent {
Self::did_send_idletimeout_warning_once().store(true, Ordering::Relaxed);
bun_core::warn!(
"Bun.serve() timed out a request after 10 seconds. Pass `idleTimeout` to configure."
);
}
}
}

fn should_add_timeout_handler_for_warning(&self) -> bool {
if DEBUG {
if !Self::did_send_idletimeout_warning_once().load(Ordering::Relaxed)
if !server_body::did_send_idletimeout_warning_once().load(Ordering::Relaxed)
&& !crate::cli::Command::get().debug.silent
{
return !self.config.has_idle_timeout;
Expand Down Expand Up @@ -759,8 +737,10 @@ impl<const SSL: bool, const DEBUG: bool> NewServer<SSL, DEBUG> {
if server.should_add_timeout_handler_for_warning() {
// We need to pass it a pointer, any pointer should do.
resp_ref.on_timeout(
Self::on_timeout_for_idle_warn,
Self::did_send_idletimeout_warning_once()
|_: *mut c_void, _: &mut uws_sys::NewAppResponse<SSL>| {
server_body::on_timeout_for_idle_warn()
},
server_body::did_send_idletimeout_warning_once()
.as_ptr()
.cast::<c_void>(),
);
Expand Down
16 changes: 6 additions & 10 deletions src/runtime/server/server_body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1203,24 +1203,20 @@ fn fetch_headers_from_js(value: JSValue, global: &JSGlobalObject) -> Option<*mut
FetchHeaders::cast_(value, global.vm()).map(|p| p.as_ptr())
}

/// Per-process latch for the dev-mode idle-timeout warning. The
/// warning is gated on `DEBUG && !silent` and only fires once globally, so a
/// single shared `AtomicBool` matches user-visible behavior.
/// Process-wide once latch for the dev-mode idle-timeout warning, shared by all protocols.
#[inline]
fn did_send_idletimeout_warning_once() -> &'static core::sync::atomic::AtomicBool {
pub(super) fn did_send_idletimeout_warning_once() -> &'static core::sync::atomic::AtomicBool {
static FLAG: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false);
&FLAG
}

/// Emits the once-only dev-mode
/// warning. Factored out as a free fn so the `RespLike::on_timeout_warn`
/// closures (which cannot name `NewServer<SSL,DEBUG>`) can call it.
fn on_timeout_for_idle_warn() {
/// The one idle-timeout warning handler for HTTP/1, HTTP/2 and HTTP/3.
pub(super) fn on_timeout_for_idle_warn() {
if !did_send_idletimeout_warning_once().swap(true, core::sync::atomic::Ordering::Relaxed)
&& !crate::cli::Command::get().debug.silent
{
bun_core::pretty_errorln!(
"<r><yellow>[Bun.serve]<r><d>:<r> request timed out after 10 seconds. Pass <d><cyan>`idleTimeout`<r> to configure."
bun_core::warn!(
"Bun.serve() timed out a request after 10 seconds. Pass `idleTimeout` to configure."
);
Output::flush();
}
Expand Down
73 changes: 73 additions & 0 deletions test/regression/issue/43016.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";

// https://github.com/oven-sh/bun/issues/43016
// The dev-mode idle timeout warning is meant to print once per process. The
// HTTP/1 path and the HTTP/2 and HTTP/3 path each had their own once flag and
// their own text, so a process that timed out one request on each protocol
// printed the warning twice.
//
// The warning is only registered when `idleTimeout` is not passed, so the
// test has to wait for the default 10 second timeout on both connections.
// That is longer than the default per-test timeout, so this test sets one.
test("Bun.serve prints the idle timeout warning once for HTTP/1 and HTTP/2 timeouts", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
import http2 from "node:http2";
import net from "node:net";

const seen = [];
const server = Bun.serve({
port: 0,
hostname: "127.0.0.1",
development: true,
http2: true,
async fetch(req) {
seen.push(req.headers.get("x-proto"));
await req.text();
return new Response("ok");
},
});

// The server closes each connection when its idle timeout fires. The
// request count proves that both requests reached the handler before
// that, so a client that failed early cannot pass as a timeout.
let remaining = 2;
function done() {
if (--remaining === 0) {
console.log("both closed, requests seen: " + seen.sort().join(","));
process.exit(0);
}
}

// HTTP/1: a POST whose body never completes.
const h1 = net.connect(server.port, "127.0.0.1", () => {
h1.write("POST / HTTP/1.1\\r\\nHost: x\\r\\nX-Proto: h1\\r\\nContent-Length: 100\\r\\n\\r\\nabc");
});
h1.on("error", () => {});
h1.on("close", done);

// HTTP/2 (cleartext, prior knowledge): a POST stream that never ends.
const session = http2.connect("http://127.0.0.1:" + server.port);
session.on("error", () => {});
session.on("close", done);
const stream = session.request({ ":method": "POST", ":path": "/", "x-proto": "h2" });
stream.on("error", () => {});
stream.write("abc");
Comment thread
robobun marked this conversation as resolved.
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("both closed, requests seen: h1,h2\n");
const warning = "warn: Bun.serve() timed out a request after 10 seconds. Pass `idleTimeout` to configure.";
expect(stderr.split(warning).length - 1).toBe(1);
expect(stderr).not.toContain("[Bun.serve]: request timed out");
expect(exitCode).toBe(0);
}, 30_000);
Comment thread
robobun marked this conversation as resolved.
Loading