Skip to content
Draft
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
5 changes: 5 additions & 0 deletions src/images/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const MAX_DECODED_BYTES_PER_IMAGE = 50 * 1024 * 1024;
const MAX_DECODED_BYTES_PER_RESPONSE = 100 * 1024 * 1024;
/** Hard cap for remote image downloads (also enforced inside pinnedHttpsGet). */
export const MAX_DOWNLOAD_BYTES = 50 * 1024 * 1024; // 50 MiB
/** Deadline for establishing TCP and TLS for provider-returned artifact URLs. */
export const DOWNLOAD_CONNECT_TIMEOUT_MS = 10_000;
/** Idle timeout for pinned HTTPS connect/headers/body when no AbortSignal is provided. */
export const DOWNLOAD_IDLE_TIMEOUT_MS = 60_000;

Expand Down Expand Up @@ -270,6 +272,7 @@ export function pinnedHttpsGet(
signal?: AbortSignal,
options?: {
maxBytes?: number;
connectTimeoutMs?: number;
idleTimeoutMs?: number;
rejectUnauthorized?: boolean;
},
Expand All @@ -279,9 +282,11 @@ export function pinnedHttpsGet(
throw new Error(`image URL must use HTTPS, got ${parsed.protocol}`);
}
const maxBytes = options?.maxBytes ?? MAX_DOWNLOAD_BYTES;
const connectTimeoutMs = options?.connectTimeoutMs ?? DOWNLOAD_CONNECT_TIMEOUT_MS;
const idleTimeoutMs = options?.idleTimeoutMs ?? DOWNLOAD_IDLE_TIMEOUT_MS;
return pinnedHttpGet(url, pinned, signal, {
maxBytes,
connectTimeoutMs,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Arm the deadline for Bun TLS handshakes

When a peer accepts TCP but never completes TLS, forwarding this value does not restore the deadline on the supported Bun 1.3.14 runtime: Bun emits the ClientRequest socket with socket.connecting === false, so pinnedHttpRequest skips creating its connect timer and the artifact download waits for the 60-second idle timeout instead. I reproduced this with a local TCP listener that accepts without speaking TLS; update the shared transport to keep HTTPS connections timed until secureConnect without gating on socket.connecting, and cover that actual socket state rather than forcing connecting: true in the mock.

AGENTS.md reference: src/AGENTS.md:L7-L9

Useful? React with 👍 / 👎.

idleTimeoutMs,
rejectUnauthorized: options?.rejectUnauthorized,
context: "image download",
Expand Down
33 changes: 32 additions & 1 deletion tests/images/pinned-https-get.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { EventEmitter } from "node:events";
import { describe, expect, mock, test } from "bun:test";
import { describe, expect, mock, spyOn, test } from "bun:test";

type LookupCb =
| ((err: Error | null, address: string, family: number) => void)
Expand Down Expand Up @@ -54,6 +54,37 @@ function installHttpsMock(bodyChunks: Buffer[], statusCode = 200) {
}

describe("pinnedHttpsGet transport", () => {
test("applies a connect deadline by default", async () => {
const requestMock = mock(() => {
const req = new EventEmitter() as EventEmitter & {
setTimeout: Function;
end: Function;
destroy: Function;
};
req.setTimeout = mock(() => {});
req.end = mock(() => {});
req.destroy = mock(() => {});
queueMicrotask(() => req.emit("socket", Object.assign(new EventEmitter(), { connecting: true })));
return req;
});
mock.module("node:https", () => ({ default: { request: requestMock }, request: requestMock }));
const timeoutSpy = spyOn(globalThis, "setTimeout").mockImplementation(((callback: () => void, ms?: number) => {
expect(ms).toBe(10_000);
queueMicrotask(callback);
return 1 as unknown as ReturnType<typeof setTimeout>;
}) as typeof setTimeout);

try {
const { pinnedHttpsGet } = await import("../../src/images/artifacts");
await expect(pinnedHttpsGet(
"https://cdn.example/hang.png",
{ address: "93.184.216.34", family: 4 },
)).rejects.toThrow(/connect timed out/);
} finally {
timeoutSpy.mockRestore();
}
});

test("lookup honors scalar and { all: true } callback shapes", async () => {
let capturedLookup: ((hostname: string, opts: unknown, cb?: LookupCb) => void) | undefined;
const requestMock = mock((options: { lookup?: typeof capturedLookup }, onResponse?: Function) => {
Expand Down
Loading