From ba7e5769feae0f09455c49a50924d4e2e6dc3e9e Mon Sep 17 00:00:00 2001 From: luvs01 Date: Sat, 8 Aug 2026 19:28:50 +0900 Subject: [PATCH] fix(auth): bound Codex warmup streams --- src/codex/warmup.ts | 8 +++++++- tests/codex-warmup.test.ts | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/codex/warmup.ts b/src/codex/warmup.ts index 9278a3d548..82edc47401 100644 --- a/src/codex/warmup.ts +++ b/src/codex/warmup.ts @@ -1,5 +1,5 @@ export class CodexWarmupError extends Error { - code: "http_status" | "missing_body" | "stream_failed" | "stream_incomplete" | "stream_error" | "invalid_sse" | "no_terminal" | "transport"; + code: "http_status" | "missing_body" | "stream_failed" | "stream_incomplete" | "stream_error" | "stream_too_large" | "invalid_sse" | "no_terminal" | "transport"; status?: number; /** Upstream error detail extracted from the response body (truncated to 512 chars). */ upstreamDetail?: string; @@ -30,6 +30,7 @@ const DEFAULT_MODEL = "gpt-5.4-mini"; const FALLBACK_MODELS = ["gpt-5.5"]; const DEFAULT_TIMEOUT_MS = 30_000; const MAX_ERROR_BODY_BYTES = 2048; +const MAX_WARMUP_STREAM_BYTES = 1024 * 1024; /** Read the first MAX_ERROR_BODY_BYTES of a response body and extract an error message. */ async function readErrorDetail(res: Response): Promise { @@ -93,11 +94,16 @@ async function drainWarmupSse(body: ReadableStream): Promise { const reader = body.getReader(); const decoder = new TextDecoder(); let buffer = ""; + let bytesRead = 0; try { for (;;) { const { done, value } = await reader.read(); if (done) break; + bytesRead += value.byteLength; + if (bytesRead > MAX_WARMUP_STREAM_BYTES) { + throw new CodexWarmupError("stream_too_large", "Codex warmup stream exceeded the size limit"); + } buffer += decoder.decode(value, { stream: true }); for (;;) { diff --git a/tests/codex-warmup.test.ts b/tests/codex-warmup.test.ts index b99eab5604..1a651ff745 100644 --- a/tests/codex-warmup.test.ts +++ b/tests/codex-warmup.test.ts @@ -60,6 +60,20 @@ describe("codex warmup", () => { .rejects.toMatchObject({ name: "CodexWarmupError", code: "invalid_sse" }); }); + test("rejects an oversized unterminated SSE stream", async () => { + const oversizedBody = new ReadableStream({ + start(controller) { + const chunk = new Uint8Array(256 * 1024).fill(65); + for (let index = 0; index < 5; index += 1) controller.enqueue(chunk); + controller.close(); + }, + }); + globalThis.fetch = (async () => new Response(oversizedBody, { status: 200 })) as typeof fetch; + + await expect(warmCodexAccount({ accessToken: "a", chatgptAccountId: "c" })) + .rejects.toMatchObject({ name: "CodexWarmupError", code: "stream_too_large" }); + }); + test("rejects EOF before success terminal", async () => { globalThis.fetch = (async () => sseResponse('event: response.created\ndata: {"type":"response.created"}\n\n')) as typeof fetch; await expect(warmCodexAccount({ accessToken: "a", chatgptAccountId: "c" }))