fix(sse): stop null-prototype sanitized errors from crashing image call logs

sanitizeUpstreamDetails() builds every object with Object.create(null) on
purpose (#12506) so a hostile upstream key such as `__proto__` can never
reach a real prototype. saveImageErrorResult() then handed that object to a
bare String(), which throws "TypeError: Cannot convert object to primitive
value" — so every Codex image failure came out as an unhandled crash
instead of the sanitized error the caller was supposed to get.

The null prototype is the correct behavior at the source, so the sink is
what has to be total: `error` is typed `unknown`, and it now serializes
objects structurally (the same way the Antigravity branch already logs its
sanitized payload) and keeps String() semantics for everything else. The
sanitized payload returned to the caller is untouched, so the redaction
guarantees are unchanged.

Covered by the three existing regression tests in
tests/unit/image-generation-handler.test.ts (codex sanitizes upstream HTTP
errors / model-access 400 retryable / ordinary 400 not retryable).
This commit is contained in:
diegosouzapw
2026-09-11 18:18:50 -03:00
parent e043682a23
commit d2a6464101

View File

@@ -2778,6 +2778,40 @@ export function saveImageSuccessResult({
};
}
/**
* Render an arbitrary `error` value as a call-log string.
*
* `saveImageErrorResult` takes `error: unknown`, and the Codex fan-out forwards
* whatever `sanitizeImageProviderError()` produced — i.e. the output of
* `sanitizeUpstreamDetails()`, which builds every object with
* `Object.create(null)` on purpose (#12506) so a hostile upstream key such as
* `__proto__` or `constructor` can never reach a real prototype. That object
* therefore has NO `toString`/`Symbol.toPrimitive`, so a bare `String(value)`
* throws `TypeError: Cannot convert object to primitive value` and turned every
* Codex image failure into an unhandled crash instead of the sanitized error.
* The null prototype is the correct behavior at the source, so the sink is what
* has to be total: serialize objects structurally (the same way the Antigravity
* branch already logs its sanitized payload) and keep `String()` semantics for
* everything else.
*/
function stringifyImageErrorForLog(value: unknown): string {
if (typeof value === "string") return value;
if (value instanceof Error) return `${value.name}: ${value.message}`;
if (value !== null && typeof value === "object") {
try {
const serialized = JSON.stringify(value);
if (typeof serialized === "string") return serialized;
} catch {
// Circular graph or a throwing toJSON — fall through to String().
}
}
try {
return String(value);
} catch {
return "[unserializable error]";
}
}
export function saveImageErrorResult({
provider,
model,
@@ -2810,7 +2844,7 @@ export function saveImageErrorResult({
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
error: typeof error === "string" ? error.slice(0, 500) : String(error).slice(0, 500),
error: stringifyImageErrorForLog(error).slice(0, 500),
requestBody,
}).catch(() => {});