fix(codex): treat OAuth 401 as unrecoverable refresh failure (#4686)

Integrated into release/v3.8.37 — codex OAuth 401 treated as unrecoverable refresh. Cherry-picked onto release tip; token-refresh-service.test.ts 38/38 green.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-25 21:26:54 -03:00
committed by GitHub
parent d773bf7720
commit a2ce88c8eb
2 changed files with 51 additions and 0 deletions

View File

@@ -1063,6 +1063,26 @@ export async function refreshCodexToken(refreshToken, log, proxyConfig: unknown
return { error: "unrecoverable_refresh_error", code: errorCode };
}
// Defense-in-depth (port from decolua/9router#1821): any 401 from OpenAI's
// OAuth token endpoint means the refresh credential itself was rejected
// (e.g. rotated away, or a payload variant whose code we do not yet
// recognize — OpenAI has shipped both `token_expired` and the bare
// "Could not validate your token" message). Retrying with the same dead
// refresh token will never succeed; surface re-auth instead of looping.
// 429 / 5xx remain transient and fall through to the retryable branch.
if (response.status === 401) {
const code = errorCode || "unauthorized";
log?.error?.(
"TOKEN_REFRESH",
"Codex OAuth token endpoint returned 401. Re-authentication required.",
{
status: response.status,
errorCode: code,
}
);
return { error: "unrecoverable_refresh_error", code };
}
log?.error?.("TOKEN_REFRESH", "Failed to refresh Codex token", {
status: response.status,
error: errorText,

View File

@@ -438,6 +438,37 @@ test("refreshCodexToken recognizes refresh_token_reused responses", async () =>
);
});
// Port from decolua/9router#1821 (sacwooky): a 401 from OpenAI's OAuth token
// endpoint means the refresh credential itself was rejected (e.g. rotated away
// or a payload whose error code we do not yet recognize). Retrying with the
// same refresh token will never succeed — surface re-auth, do not loop.
test("refreshCodexToken treats any 401 from the token endpoint as unrecoverable", async () => {
const log = createLog();
await withMockedFetch(
async () =>
textResponse(
JSON.stringify({
error: {
// A payload variant whose code/type are NOT in the existing
// unrecoverable set — only the 401 status proves the token is dead.
message: "Could not validate your token. Please try signing in again.",
type: "invalid_request_error",
},
}),
401
),
async () => {
const result = await refreshCodexToken("codex-refresh", log);
assert.equal(
result?.error,
"unrecoverable_refresh_error",
"401 from OpenAI token endpoint must surface re-auth instead of returning null (which triggers retry)"
);
}
);
});
test("refreshKiroToken uses the AWS OIDC flow when client credentials are present", async () => {
const log = createLog();
const calls: any[] = [];