Files
OmniRoute/open-sse/utils/tlsFirstByteWatchdog.ts
Diego Rodrigues de Sa e Souza 1361a9dd88 fix(sse): add first-byte watchdog to the TLS-fingerprint transport (#12656) (#13272)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:06:09 -03:00

116 lines
3.8 KiB
TypeScript

import { getTlsFirstByteWatchdogMs } from "@/shared/utils/runtimeTimeouts";
// #12656 — the wreq-js TLS-fingerprint transport resolves the Response as
// soon as upstream headers arrive, with zero protection around how long the
// caller then waits for the body's first byte. The only timing guard on that
// path, TlsClient's flat `timeout`, defaults to 600_000ms — matching the
// reported 90-600s stall window exactly. This module races the body's first
// `read()` against a short, env-overridable watchdog: a healthy body is
// completely unaffected (bytes already buffered are replayed through a
// passthrough stream, nothing is dropped), while a body that never yields
// within the deadline cancels the wreq reader and throws so the caller
// (proxyFetch's existing TLS-fallback catch blocks) can fall back to the
// direct/proxy dispatcher instead of hanging for minutes.
export const TLS_FIRST_BYTE_WATCHDOG_TIMEOUT_CODE = "TLS_FIRST_BYTE_WATCHDOG_TIMEOUT";
type BodyReader = ReadableStreamDefaultReader<Uint8Array>;
type FirstReadResult = ReadableStreamReadResult<Uint8Array>;
function createWatchdogTimeoutError(timeoutMs: number): Error & { code: string } {
const err = new Error(
`TLS fingerprint transport produced no first byte within ${timeoutMs}ms`
) as Error & { code: string };
err.name = "TimeoutError";
err.code = TLS_FIRST_BYTE_WATCHDOG_TIMEOUT_CODE;
return err;
}
export function isTlsFirstByteWatchdogTimeout(err: unknown): boolean {
return (
!!err &&
typeof err === "object" &&
"code" in err &&
(err as { code?: unknown }).code === TLS_FIRST_BYTE_WATCHDOG_TIMEOUT_CODE
);
}
async function raceFirstChunk(reader: BodyReader, timeoutMs: number): Promise<FirstReadResult> {
let timer: ReturnType<typeof setTimeout> | undefined;
const timeoutPromise = new Promise<never>((_, reject) => {
timer = setTimeout(() => reject(createWatchdogTimeoutError(timeoutMs)), timeoutMs);
timer.unref?.();
});
try {
return await Promise.race([reader.read(), timeoutPromise]);
} finally {
clearTimeout(timer);
}
}
async function pumpRemainingChunks(
reader: BodyReader,
controller: ReadableStreamDefaultController<Uint8Array>
): Promise<void> {
try {
for (;;) {
const { done, value } = await reader.read();
if (done) {
controller.close();
return;
}
if (value) controller.enqueue(value);
}
} catch (error) {
controller.error(error);
}
}
function buildPassthroughStream(
reader: BodyReader,
firstChunk: FirstReadResult
): ReadableStream<Uint8Array> {
return new ReadableStream<Uint8Array>({
start(controller) {
if (firstChunk.value) controller.enqueue(firstChunk.value);
if (firstChunk.done) {
controller.close();
return;
}
void pumpRemainingChunks(reader, controller);
},
cancel(reason) {
void reader.cancel(reason).catch(() => {});
},
});
}
/**
* Guard a TLS-fingerprint Response's first body byte with a short watchdog.
* Resolves with an equivalent Response (status/headers preserved) whose body
* has already produced at least one byte, or throws
* TLS_FIRST_BYTE_WATCHDOG_TIMEOUT after cancelling the reader so the caller
* can fall back to another transport.
*/
export async function guardTlsFirstByte(
response: Response,
timeoutMs: number = getTlsFirstByteWatchdogMs()
): Promise<Response> {
if (!timeoutMs || timeoutMs <= 0 || !response.body) return response;
const reader = response.body.getReader();
let firstChunk: FirstReadResult;
try {
firstChunk = await raceFirstChunk(reader, timeoutMs);
} catch (error) {
await reader.cancel(error).catch(() => {});
throw error;
}
return new Response(buildPassthroughStream(reader, firstChunk), {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
}