diff --git a/packages/bun-uws/src/HttpContext.h b/packages/bun-uws/src/HttpContext.h index 6e3bd96fe15d..03b4dd81b802 100644 --- a/packages/bun-uws/src/HttpContext.h +++ b/packages/bun-uws/src/HttpContext.h @@ -1007,8 +1007,15 @@ struct HttpContext { us_listen_socket_t *listen(struct ssl_ctx_st *sslCtx, const char *host, int port, int options) { int error = 0; /* HTTP clients always send first (the request, or ClientHello for TLS), so defer - * accept() until data arrives and dispatch the read immediately after accept. */ - auto socket = us_socket_group_listen(&group, socketKind(), sslCtx, host, port, options | LIBUS_LISTEN_DEFER_ACCEPT, socketExtSize(), &error); + * accept() until data arrives and dispatch the read immediately after accept. + * Skip this for node:http compat, where the 'connection' event and + * server.setTimeout are measured from TCP accept, not first byte: a kernel-side + * defer would make both fire up to a second late for a client that connects and + * sends nothing. */ + if (!isNodeHttp()) { + options |= LIBUS_LISTEN_DEFER_ACCEPT; + } + auto socket = us_socket_group_listen(&group, socketKind(), sslCtx, host, port, options, socketExtSize(), &error); // we dont depend on libuv ref for keeping it alive if (socket) { us_socket_unref(&socket->s); diff --git a/test/js/node/http/node-http-server-timeouts.test.ts b/test/js/node/http/node-http-server-timeouts.test.ts index 98700bbdbec0..0889beed9b49 100644 --- a/test/js/node/http/node-http-server-timeouts.test.ts +++ b/test/js/node/http/node-http-server-timeouts.test.ts @@ -123,6 +123,50 @@ describe("node:http server timeout enforcement", () => { } }); + test("server.setTimeout() fires close to the configured time for a client that never sends", async () => { + // A client that connects and never writes: 'connection' must fire on TCP + // accept and the inactivity timer must start then, not after a kernel-side + // deferred-accept window. + const server = http.createServer((req, res) => res.end("ok")); + let connectionAt: number | undefined; + let timeoutAt: number | undefined; + server.on("connection", () => { + connectionAt ??= Date.now() - t0; + }); + server.setTimeout(500, socket => { + timeoutAt ??= Date.now() - t0; + socket.destroy(); + }); + const port = await listen(server); + let t0 = Date.now(); + try { + const { promise: closed, resolve: onClosed } = Promise.withResolvers(); + const socket = net.connect(port, "127.0.0.1"); + socket.on("error", () => {}); + socket.resume(); + socket.on("connect", () => { + t0 = Date.now(); + }); + socket.on("close", () => onClosed(Date.now() - t0)); + const closedAt = await closed; + // The deferred-accept window is a full second at minimum, so these bounds + // distinguish "accepted immediately" from "accepted after defer" without + // being tight enough to flake under ASAN. + expect({ + connectionSeenPromptly: connectionAt !== undefined && connectionAt < 900, + timeoutNearConfigured: timeoutAt !== undefined && timeoutAt < 1250, + closedNearConfigured: closedAt < 1250, + }).toEqual({ + connectionSeenPromptly: true, + timeoutNearConfigured: true, + closedNearConfigured: true, + }); + } finally { + server.closeAllConnections(); + server.close(); + } + }); + test("keepAliveTimeout closes an idle keep-alive connection after the response", async () => { const server = http.createServer((req, res) => { req.resume();