From de2b464c3ffb91272076d143fc7bacc57bfe9630 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Tue, 14 Jul 2026 21:19:16 -0300 Subject: [PATCH] fix: sanitize non-Latin1 chars in combo diagnostic headers (#6612) (#7190) --- ...mbo-diagnostics-header-bytestring-crash.md | 1 + open-sse/utils/error.ts | 28 +++++++++++--- tests/unit/combo-diagnostics-trace.test.ts | 38 +++++++++++++++++++ 3 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 changelog.d/fixes/6612-combo-diagnostics-header-bytestring-crash.md diff --git a/changelog.d/fixes/6612-combo-diagnostics-header-bytestring-crash.md b/changelog.d/fixes/6612-combo-diagnostics-header-bytestring-crash.md new file mode 100644 index 0000000000..77f12ced1f --- /dev/null +++ b/changelog.d/fixes/6612-combo-diagnostics-header-bytestring-crash.md @@ -0,0 +1 @@ +- fix(sse): sanitize non-Latin1 characters before embedding combo diagnostics in HTTP headers, preventing a ByteString crash on quality-check failure (#6612) diff --git a/open-sse/utils/error.ts b/open-sse/utils/error.ts index 88efa6c67d..be3de82dda 100644 --- a/open-sse/utils/error.ts +++ b/open-sse/utils/error.ts @@ -145,6 +145,22 @@ function clampDiagStr(v: unknown, max = 128): string { return typeof v === "string" ? v.slice(0, max).replace(/[\r\n]+/g, " ") : ""; } +/** + * HTTP header values must be Latin1/ByteString (undici throws a TypeError + * otherwise — see #6612). Replace any codepoint outside the Latin1 range + * (0-255) with "?" so header construction never throws. Only used for the + * literal header value; the JSON body keeps the original, unsanitized + * readable text via `sanitizeComboDiagnostics`. + */ +function toHeaderSafeAscii(v: string): string { + let out = ""; + for (let i = 0; i < v.length; i++) { + const code = v.charCodeAt(i); + out += code > 255 ? "?" : v[i]; + } + return out; +} + /** * Whitelist projection — guarantees only id/reason string primitives + integer * counts can escape, regardless of what the caller assembled. This is the secret @@ -186,10 +202,12 @@ export function errorResponseWithComboDiagnostics( if (opts.code) body.error.code = opts.code; if (opts.type) body.error.type = opts.type; body.diagnostics = safe; - const excludedHeader = safe.excluded - .map((e) => `${e.provider}${e.model ? `/${e.model}` : ""}:${e.reason}`) - .join(",") - .slice(0, 900); + const excludedHeader = toHeaderSafeAscii( + safe.excluded + .map((e) => `${e.provider}${e.model ? `/${e.model}` : ""}:${e.reason}`) + .join(",") + .slice(0, 900) + ); return new Response(JSON.stringify(body), { status: statusCode, headers: { @@ -197,7 +215,7 @@ export function errorResponseWithComboDiagnostics( "x-omniroute-combo-pool-size": String(safe.poolSize), "x-omniroute-combo-attempted": String(safe.attempted), "x-omniroute-combo-excluded": excludedHeader, - "x-omniroute-combo-terminal-reason": safe.terminalReason.slice(0, 200), + "x-omniroute-combo-terminal-reason": toHeaderSafeAscii(safe.terminalReason.slice(0, 200)), }, }); } diff --git a/tests/unit/combo-diagnostics-trace.test.ts b/tests/unit/combo-diagnostics-trace.test.ts index be25e64c77..fe8bb546a4 100644 --- a/tests/unit/combo-diagnostics-trace.test.ts +++ b/tests/unit/combo-diagnostics-trace.test.ts @@ -79,3 +79,41 @@ test("combo diagnostics: secret containment — non-whitelisted fields never sur assert.ok(!serialized.includes("accessToken"), "no accessToken KEY survives"); assert.ok(!serialized.includes("token"), "no token KEY survives"); }); + +test("combo diagnostics: terminalReason with a non-Latin1 char (em dash) must not crash Response construction (#6612)", () => { + const terminalReason = "reasoning consumed 5/5 tokens — no content output"; + assert.doesNotThrow(() => { + const res = errorResponseWithComboDiagnostics( + 502, + `Upstream response failed quality validation: ${terminalReason}`, + { + poolSize: 4, + attempted: 1, + excluded: [{ provider: "deepseek", model: "deepseek-v4-flash-free", reason: "quality — bad" }], + attemptOrder: [{ provider: "deepseek", model: "deepseek-v4-flash-free" }], + terminalReason, + } + ); + assert.equal(res.status, 502); + }); +}); + +test("combo diagnostics: JSON body keeps the original non-Latin1 text even though headers are ASCII-sanitized (#6612)", async () => { + const terminalReason = "reasoning consumed 5/5 tokens — no content output"; + const res = errorResponseWithComboDiagnostics( + 502, + `Upstream response failed quality validation: ${terminalReason}`, + { + poolSize: 1, + attempted: 1, + excluded: [], + attemptOrder: [{ provider: "deepseek", model: "deepseek-v4-flash-free" }], + terminalReason, + } + ); + // Header value must be a valid Latin1 ByteString — em dash (U+2014) replaced. + assert.equal(res.headers.get("x-omniroute-combo-terminal-reason"), terminalReason.replace("—", "?")); + const body = await res.json(); + // JSON body keeps the original, readable (unsanitized) em dash. + assert.equal(body.diagnostics.terminalReason, terminalReason); +});