Files
OmniRoute/tests/unit/bug-8020-codex-peek-body-timeout.test.ts
Diego Rodrigues de Sa e Souza 5e234d503d fix(sse): bound Codex SSE peek read with per-read timeout (#8020) (#8043)
peekCodexSseTransientError() ran before chatCore's normal
readiness/idle-timeout pipeline and read the first SSE chunk with a
bare reader.read() — no timeout wrapper. A 200 text/event-stream body
that never emitted a byte hung for ~15min (901399ms observed) before
the platform killed the connection and surfaced a generic 502.

Wrap the peek loop's read and the re-assembled passthrough body's
pull() in readStreamChunkWithTimeout, bounded PER READ (not a total
deadline) so a long-but-alive reasoning stream keeps resetting the
window on every chunk it emits. On timeout the reader is cancelled and
the request now fails fast with a 504 instead of hanging.

New small module open-sse/executors/codex/bodyTimeout.ts holds the
wrapping helpers to keep codex.ts within its frozen size baseline.
2026-07-21 21:41:58 -03:00

72 lines
2.7 KiB
TypeScript

// #8020: `peekCodexSseTransientError()`'s first-chunk read (open-sse/executors/codex.ts) had
// NO timeout wrapper — a 200 text/event-stream response whose body never emits a byte hung on a
// bare `reader.read()` for ~15 minutes (901399ms observed) before the platform killed the
// connection and surfaced a generic 502. This runs BEFORE chatCore's normal readiness/idle-timeout
// pipeline takes over, so FETCH_BODY_TIMEOUT_MS / STREAM_IDLE_TIMEOUT_MS never applied to it.
//
// Fix: every read in the peek loop and the re-assembled passthrough body is now bounded by a
// PER-READ timeout (open-sse/executors/codex/bodyTimeout.ts), so a stalled body settles fast
// instead of hanging, while a long-but-alive stream that keeps emitting chunks never trips it.
import test from "node:test";
import assert from "node:assert/strict";
import { peekCodexSseTransientError } from "../../open-sse/executors/codex.ts";
// Small, explicit override — never depends on the ~120s/600s production default, so this test
// settles fast and deterministically regardless of env configuration.
const TEST_TIMEOUT_MS = 200;
function stuckSseResponse(): Response {
const stuck = new ReadableStream<Uint8Array>({
pull() {
// Never enqueue and never close — simulates an upstream body that goes silent.
},
});
return new Response(stuck, {
status: 200,
headers: { "content-type": "text/event-stream" },
});
}
test(
"peekCodexSseTransientError does not hang forever on a silently stuck SSE body (#8020)",
{ timeout: 5000 },
async () => {
const start = Date.now();
const peek = await peekCodexSseTransientError(stuckSseResponse(), TEST_TIMEOUT_MS);
const elapsed = Date.now() - start;
assert.ok(
elapsed < 2000,
`expected peek to settle within 2000ms via a body timeout, took ${elapsed}ms`
);
assert.equal(peek.timedOut, true, "expected the peek to report timedOut on a stalled body");
assert.equal(peek.matched, null);
assert.equal(peek.replacementBody, null);
}
);
test(
"peekCodexSseTransientError still detects a transient error when the body responds promptly",
async () => {
const encoder = new TextEncoder();
const response = new Response(
new ReadableStream<Uint8Array>({
pull(controller) {
controller.enqueue(
encoder.encode(
'event: error\ndata: {"error":{"message":"Selected model is at capacity."}}\n\n'
)
);
controller.close();
},
}),
{ status: 200, headers: { "content-type": "text/event-stream" } }
);
const peek = await peekCodexSseTransientError(response, TEST_TIMEOUT_MS);
assert.equal(peek.timedOut ?? false, false);
assert.match(peek.matched ?? "", /capacity/);
}
);