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
11 changes: 9 additions & 2 deletions packages/bun-uws/src/HttpContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
44 changes: 44 additions & 0 deletions test/js/node/http/node-http-server-timeouts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number>();
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();
Expand Down
Loading