Skip to content
Closed
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
6 changes: 3 additions & 3 deletions src/lib/pinned-http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ function pinnedHttpRequest(
req.on("socket", (socket) => {
const connectedEvent = parsed.protocol === "https:" ? "secureConnect" : "connect";
if (!socket.connecting) {
if (!usesLegacyIdleTimeout) startFirstByteTimer();
startFirstByteTimer();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve zero as the disabled legacy timeout

When a caller passes idleTimeoutMs: 0 to disable the legacy socket timeout, firstByteTimeoutMs also becomes zero and this unconditional call schedules an immediate JavaScript timer. Consequently, any response whose headers arrive on a later turn is rejected with first_byte_timeout; before this commit, req.setTimeout(0) and response.setTimeout(0) disabled the timeout and the same request succeeded. Preserve the established zero-timeout behavior by skipping the absolute timer when the inherited legacy value is zero (or otherwise normalize disabled legacy timeouts).

AGENTS.md reference: src/AGENTS.md:L10-L10

Useful? React with 👍 / 👎.

return;
}
if (connectTimeoutMs !== undefined) {
Expand All @@ -210,7 +210,7 @@ function pinnedHttpRequest(
}
socket.once(connectedEvent, () => {
clearConnectTimer();
if (!usesLegacyIdleTimeout) startFirstByteTimer();
startFirstByteTimer();
});
socket.once("error", () => {
clearConnectTimer();
Expand Down Expand Up @@ -262,4 +262,4 @@ export function pinnedHttpPost(
options?: PinnedHttpRequestOptions,
): Promise<Response> {
return pinnedHttpRequest(url, pinned, "POST", body, signal, options);
}
}
48 changes: 48 additions & 0 deletions tests/pinned-http.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { createServer, type Server, type Socket } from "node:net";
import { afterEach, describe, expect, test } from "bun:test";
import { pinnedHttpGet } from "../src/lib/pinned-http";

let server: Server | undefined;
const sockets = new Set<Socket>();

afterEach(async () => {
for (const socket of sockets) socket.destroy();
sockets.clear();
if (server) {
const closing = server;
server = undefined;
await new Promise<void>((resolve) => closing.close(() => resolve()));
}
});

describe("pinned HTTP timeouts", () => {
test("legacy idle timeout remains an absolute response-header deadline", async () => {
server = createServer((socket) => {
sockets.add(socket);
socket.on("close", () => sockets.delete(socket));
socket.write("HTTP/1.1 200 OK\r\nX-Slow: ");
const drip = setInterval(() => socket.write("x"), 10);
socket.on("close", () => clearInterval(drip));
});
const port = await new Promise<number>((resolve, reject) => {
server!.once("error", reject);
server!.listen(0, "127.0.0.1", () => {
const address = server!.address();
if (!address || typeof address === "string") {
reject(new Error("test server did not expose a TCP port"));
return;
}
resolve(address.port);
});
});

const request = pinnedHttpGet(
`http://slow-header.invalid:${port}/`,
{ address: "127.0.0.1", family: 4 },
undefined,
{ idleTimeoutMs: 50 },
);

await expect(request).rejects.toThrow(/timed out/);
});
});
Loading