Skip to content
Merged
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: 3 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,9 +437,10 @@ async function readBoundedExternalJsonResponse(response: Response): Promise<Uint
const bytes = new Uint8Array(maxExternalJsonResponseBytes);
let totalBytes = 0;
let timeoutHandle!: ReturnType<typeof setTimeout>;
const ignoreCancellationFailure = () => undefined;
const deadline = new Promise<never>((_, reject) => {
timeoutHandle = setTimeout(() => {
void reader.cancel().catch(() => undefined);
void reader.cancel().catch(ignoreCancellationFailure);
reject(new SyntaxError("JSON response read timed out"));
}, externalJsonResponseReadDeadlineMs);
});
Expand All @@ -448,7 +449,7 @@ async function readBoundedExternalJsonResponse(response: Response): Promise<Uint
const result = await reader.read();
if (result.done) break;
if (result.value.byteLength > maxExternalJsonResponseBytes - totalBytes) {
await reader.cancel();
void reader.cancel().catch(ignoreCancellationFailure);
throw new SyntaxError("JSON response exceeded byte limit");
}
bytes.set(result.value, totalBytes);
Expand Down
79 changes: 79 additions & 0 deletions test/oidc-external-json-read-deadline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,85 @@ describe("GitHub OIDC external JSON response deadline", () => {
postFailureReader.releaseLock();
});

it("rejects an oversized discovery response without awaiting stalled cancellation cleanup", async () => {
vi.useFakeTimers();
vi.resetModules();
const { default: worker } = await import("../src/index");

const cancel = vi.fn(() => new Promise<void>(() => undefined));
const body = new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(new Uint8Array(65_537));
},
cancel,
}, { highWaterMark: 0 });
mockDiscoveryResponse(body);

const exchange = startExchange(worker);
const outcome = Promise.race([
exchange.then((response) => ({ kind: "response" as const, response })),
new Promise<{ kind: "failsafe" }>((resolve) => {
setTimeout(() => resolve({ kind: "failsafe" }), 100);
}),
]);

await vi.advanceTimersByTimeAsync(100);
const result = await outcome;

expect(result.kind).toBe("response");
if (result.kind !== "response") return;
expect(cancel).toHaveBeenCalledOnce();
expect(result.response.status).toBe(502);
await expect(result.response.json()).resolves.toMatchObject({
ok: false,
error_code: "ERR_OIDC_VERIFICATION",
message: "GitHub OIDC discovery document was not valid JSON",
});

const postFailureReader = body.getReader();
postFailureReader.releaseLock();
});

it("keeps the oversize classification and releases the reader lock when cancellation rejects", async () => {
vi.useFakeTimers();
vi.resetModules();
const { default: worker } = await import("../src/index");

const cancel = vi.fn(() => Promise.reject(new Error("cancel cleanup failed")));
const body = new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(new Uint8Array(65_537));
},
cancel,
}, { highWaterMark: 0 });
mockDiscoveryResponse(body);

const exchange = startExchange(worker);
const outcome = Promise.race([
exchange.then((response) => ({ kind: "response" as const, response })),
new Promise<{ kind: "failsafe" }>((resolve) => {
setTimeout(() => resolve({ kind: "failsafe" }), 100);
}),
]);

await vi.advanceTimersByTimeAsync(100);
await Promise.resolve();
const result = await outcome;

expect(result.kind).toBe("response");
if (result.kind !== "response") return;
expect(cancel).toHaveBeenCalledOnce();
expect(result.response.status).toBe(502);
await expect(result.response.json()).resolves.toMatchObject({
ok: false,
error_code: "ERR_OIDC_VERIFICATION",
message: "GitHub OIDC discovery document was not valid JSON",
});

const postFailureReader = body.getReader();
postFailureReader.releaseLock();
});

it("keeps one absolute deadline while a peer trickles bytes", async () => {
vi.useFakeTimers();
vi.resetModules();
Expand Down
Loading