diff --git a/open-sse/utils/errorPathRedaction.ts b/open-sse/utils/errorPathRedaction.ts index 2b372af583..cdf33fc4dc 100644 --- a/open-sse/utils/errorPathRedaction.ts +++ b/open-sse/utils/errorPathRedaction.ts @@ -464,8 +464,8 @@ function findUnquotedPathEnd( let hasFilesystemEvidence = false; let hasUnresolvedFragments = false; - const resolveEndpoint = (): number => { - if (hasUnresolvedFragments) { + const resolveEndpoint = (ignoreAmbiguity = false): number => { + if (hasUnresolvedFragments && !ignoreAmbiguity) { return failClosedAmbiguity || hasFilesystemEvidence ? value.length : -1; } if (resolvedExtensionEnd >= 0) return resolvedExtensionEnd; @@ -530,7 +530,12 @@ function findUnquotedPathEnd( while (nextTokenStart < value.length && isWhitespace(value[nextTokenStart])) nextTokenStart++; if (nextTokenStart >= value.length) return resolveEndpoint(); if (isSyntacticallyAbsolutePathAt(value, nextTokenStart)) { - const endpoint = resolveEndpoint(); + // A route-shielded upcoming span (e.g. "POST /v1/foo") is never + // filesystem-sensitive by design — see hasRouteContextBefore. Its mere + // presence must not force ambiguous prose in between (like "Use POST") + // to fail closed and swallow past it into the shielded route and + // beyond; resolve with whatever evidence was already gathered instead. + const endpoint = resolveEndpoint(hasRouteContextBefore(value, nextTokenStart)); if (endpoint >= 0) return endpoint; return acceptEndpointBeforeAnotherAbsolute ? lastPathTokenEnd : -1; } diff --git a/tests/unit/error-message-sanitization.test.ts b/tests/unit/error-message-sanitization.test.ts index 8813e7ac71..21f989ef9e 100644 --- a/tests/unit/error-message-sanitization.test.ts +++ b/tests/unit/error-message-sanitization.test.ts @@ -239,6 +239,21 @@ test("sanitizeErrorMessage replaces absolute paths with ", async () => { assert.ok(out2.includes("")); }); +test("sanitizeErrorMessage does not swallow a shielded route hint that follows an earlier redacted path (#6457)", async () => { + // Regression: an unshielded route-looking span ("on /v1/chat/completions") + // followed by ambiguous prose ("Use POST") used to make the unquoted-path + // scanner fail closed all the way to the end of the string, deleting a + // second, legitimately-shielded route reference ("POST /v1/images/...") + // and everything after it instead of just redacting the first span. + const { sanitizeErrorMessage } = await import("../../open-sse/utils/error.ts"); + const input = + "Model 'x' is an image-generation model and cannot be used on /v1/chat/completions. Use POST /v1/images/generations instead."; + const out = sanitizeErrorMessage(input); + assert.match(out, /\/v1\/images\/generations/, "shielded route hint must survive"); + assert.match(out, /instead\.$/, "text after the shielded route hint must not be dropped"); + assert.ok(out.includes(""), "the earlier unshielded route span is still redacted"); +}); + test("sanitizeErrorMessage handles non-string inputs safely", async () => { const { sanitizeErrorMessage } = await import("../../open-sse/utils/error.ts"); assert.equal(sanitizeErrorMessage(undefined), "");