fix(sse): don't mark a valid Qoder PAT expired on a generic Cosy 500 (#3247) (#3283)

A working Qoder PAT was reported as "expired". The validator probes the Cosy endpoint
(api1.qoder.sh) — which IS the correct PAT path (the executor falls back to it after the
expected 401 from api.qoder.com). The bug was the verdict: isCosyAppError (added by #2860)
marked ANY Cosy 500 with "success":false as an auth failure, including a generic
{..."msgCode":500,"message":"Internal Server Error"} server fault — contradicting the
older #1391 "5xx = valid bypass" rule.

Narrow it: a Cosy 500 only marks the PAT invalid when the body carries an EXPLICIT auth
signal (unauthorized/forbidden/expired/token invalid/...); a generic Internal Server Error
falls back to valid-bypass. #2860's protection for genuine auth rejections is preserved.

Regression test: tests/unit/qoder-cli.test.ts — the two pre-existing generic-500 cases now
assert valid:true (they encoded the #3247 bug) + a new explicit-auth-signal case asserts
valid:false. 13/13 green.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-06 03:11:58 -03:00
committed by GitHub
parent 36932b62a7
commit a7e445edea
3 changed files with 44 additions and 15 deletions

View File

@@ -49,6 +49,7 @@ Thanks to everyone whose work landed in v3.8.12:
- **sse/groq:** non-reasoning Groq models (`llama-3.3-70b-versatile`, `llama-4-scout`) are now flagged `supportsReasoning: false`, so `reasoning_effort` / `output_config.effort` / `thinking` are stripped before dispatch instead of being forwarded and rejected with HTTP 400 — fixes the Claude Code → Groq regression of #764 ([#3258](https://github.com/diegosouzapw/OmniRoute/issues/3258))
- **api/images:** `POST /v1/images/edits` to a custom OpenAI-compatible provider no longer forwards an empty `model`. The multipart body is now built as a `Buffer` with an explicit boundary instead of a global `FormData` — the patched undici `fetch` serialized a native `FormData` as the literal string `[object FormData]` (text/plain), dropping every field including `model` ([#3273](https://github.com/diegosouzapw/OmniRoute/issues/3273))
- **api/webhooks:** webhook URLs may now target a private/internal address (e.g. `192.168.x`, a docker-internal host) when `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS=true` — the webhook guard reuses the same explicit opt-in as private provider URLs (default OFF; protocol and embedded-credential checks stay unconditional). Cloud-metadata / link-local endpoints (`169.254.169.254`, `metadata.google.internal`, `100.100.100.200`, `169.254.0.0/16`) are blocked **unconditionally** even with the opt-in on, and the webhook test endpoint redacts the upstream response body for private targets (no SSRF→IAM-credential pivot, no content exfiltration) ([#3269](https://github.com/diegosouzapw/OmniRoute/issues/3269))
- **sse/qoder:** a valid Qoder Personal Access Token is no longer wrongly reported as "expired" when the Cosy validation endpoint returns a generic `Internal Server Error` (HTTP 500). A Cosy 500 only marks the PAT invalid when its body carries an explicit auth signal; a generic server fault now falls back to the #1391 valid-bypass rule ([#3247](https://github.com/diegosouzapw/OmniRoute/issues/3247))
---

View File

@@ -503,18 +503,24 @@ export async function validateQoderCliPat({
return { valid: true, error: null, unsupported: false };
}
// Treat 5xx as valid bypass to prevent false negatives from legacy Qoder APIs (issue #1391)
// Treat 5xx as a valid bypass to prevent false negatives from legacy Qoder APIs (#1391).
// A Cosy `{"success":false}` 500 is ambiguous: it can be a genuine auth rejection OR a
// transient/generic upstream "Internal Server Error". Only mark the PAT invalid when the
// body carries an EXPLICIT auth signal — a generic 500 is a server fault, not an auth
// verdict, so a working PAT must not be reported as expired (#3247, narrowing #2860).
if (res.status >= 500) {
const isCosyAppError =
/"success"\s*:\s*false/.test(errorDetail) &&
(/"msgCode"\s*:\s*500/.test(errorDetail) || /internal\s*server\s*error/i.test(errorDetail));
const isCosyResponse = /"success"\s*:\s*false/.test(errorDetail);
const hasAuthSignal =
/(unauthorized|forbidden|expired|revoked|not\s*authorized|permission\s*denied|access\s*denied|invalid\s*(?:token|credential|api[\s_-]*key)|token\s*(?:invalid|expired|revoked))/i.test(
errorDetail
);
if (isCosyAppError) {
if (isCosyResponse && hasAuthSignal) {
return {
valid: false,
error:
`Authentication failed (HTTP ${res.status}). The Qoder Cosy server returned an Internal Server Error. ` +
"This typically indicates that your Personal Access Token is invalid, expired, or not authorized. " +
`Authentication failed (HTTP ${res.status}). The Qoder Cosy server rejected the token ` +
"as invalid, expired, or not authorized. " +
"Please check your token at https://qoder.com/account/integrations." +
(errorDetail ? ` Server response: ${errorDetail}` : ""),
unsupported: false,

View File

@@ -365,37 +365,59 @@ test("validateQoderCliPat treats 5xx HTTP failures as valid bypass", async () =>
}
});
test("validateQoderCliPat rejects 500 HTTP failures if response is Cosy app-level auth error", async () => {
// #3247: a generic Cosy 500 (`{"success":false,...,"msgCode":500,"message":"Internal
// Server Error"}`) is a SERVER fault, not a reliable auth verdict — a PAT that works in
// the Qoder CLI was being wrongly marked "expired". Per the older #1391 rule, a generic
// 5xx is now a valid bypass; only an explicit auth signal in the body marks it invalid.
test("validateQoderCliPat treats a generic Cosy 500 (no auth signal) as a valid bypass (#3247)", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async (url) => {
if (String(url).includes("/ping")) return new Response("pong", { status: 200 });
return new Response(
' { "success" : false, "traceId": "a4e5de61929400b9243b4f6e49756906", "msgCode" : 500 } ',
'{"success":false,"traceId":"a4e5de61929400b9243b4f6e49756906","msgCode":500,"msgInfo":"Internal Server Error","message":"Internal Server Error"}',
{ status: 500 }
);
};
try {
const result = await qoderCli.validateQoderCliPat({ apiKey: "invalid-pat" });
assert.equal(result.valid, false);
assert.match(result.error!, /Authentication failed \(HTTP 500\)/);
const result = await qoderCli.validateQoderCliPat({ apiKey: "pt-valid-token" });
assert.equal(result.valid, true);
assert.match(result.error!, /treating PAT as valid/);
} finally {
globalThis.fetch = originalFetch;
}
});
test("validateQoderCliPat rejects 500 HTTP failures using regex for Internal Server Error with whitespace", async () => {
test("validateQoderCliPat treats a generic 'Internal Server Error' 500 as a valid bypass (#3247)", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async (url) => {
if (String(url).includes("/ping")) return new Response("pong", { status: 200 });
return new Response(' { "success" : false, "error": "Internal Server Error" } ', {
status: 500,
});
};
try {
const result = await qoderCli.validateQoderCliPat({ apiKey: "pt-valid-token" });
assert.equal(result.valid, true);
assert.match(result.error!, /treating PAT as valid/);
} finally {
globalThis.fetch = originalFetch;
}
});
test("validateQoderCliPat still rejects a Cosy 500 that carries an explicit auth signal (#2860)", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async (url) => {
if (String(url).includes("/ping")) return new Response("pong", { status: 200 });
return new Response(
' { "success" : false, "error": "Internal Server Error" } ',
'{"success":false,"msgCode":500,"message":"token invalid or unauthorized"}',
{ status: 500 }
);
};
try {
const result = await qoderCli.validateQoderCliPat({ apiKey: "invalid-pat" });
const result = await qoderCli.validateQoderCliPat({ apiKey: "pt-bad-token" });
assert.equal(result.valid, false);
assert.match(result.error!, /Authentication failed \(HTTP 500\)/);
} finally {