{"record":{"id":"125978890338f9a9","repo":"moeru-ai/airi","slug":"token-refresh-failed-response-status","errorCode":null,"errorMessage":"Token refresh failed: ${response.status}","messagePattern":"Token refresh failed: (.+?)","errorType":"http","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stage-ui/src/libs/auth-oidc.ts","lineNumber":140,"sourceCode":"    grant_type: 'refresh_token',\n    refresh_token: refreshToken,\n    client_id: clientId,\n    resource: SERVER_URL,\n  }\n\n  if (clientSecret)\n    params.client_secret = clientSecret\n\n  const body = new URLSearchParams(params)\n\n  const response = await fetch(new URL(OIDC_TOKEN_PATH, SERVER_URL), {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n    body,\n  })\n\n  if (!response.ok)\n    throw new Error(`Token refresh failed: ${response.status}`)\n\n  return await response.json()\n}\n\n// Session storage keys for PKCE flow state (survives page navigation during OAuth)\nconst FLOW_STATE_KEY = 'auth/v1/oidc-flow-state'\nconst FLOW_PARAMS_KEY = 'auth/v1/oidc-flow-params'\n\n/**\n * Persist OIDC flow state before navigating to the authorization server.\n */\nexport function persistFlowState(flowState: OIDCFlowState, params: OIDCFlowParams): void {\n  sessionStorage.setItem(FLOW_STATE_KEY, JSON.stringify(flowState))\n  sessionStorage.setItem(FLOW_PARAMS_KEY, JSON.stringify(params))\n}\n\n/**\n * Retrieve and clear persisted OIDC flow state after callback.","sourceCodeStart":122,"sourceCodeEnd":158,"githubUrl":"https://github.com/moeru-ai/airi/blob/27111382b4a79a7e983289d6e983a06af185ed0f/packages/stage-ui/src/libs/auth-oidc.ts#L122-L158","documentation":"Thrown by refreshAccessToken() when the token endpoint returns non-2xx during a refresh-token grant (RFC 6749 S6). Unlike the exchange error, this variant includes only the status code, not the body — callers must treat it as 'the refresh token is no longer usable'. It is a pure function; the caller decides whether to force re-authentication.","triggerScenarios":"POST to /api/auth/oauth2/token with grant_type=refresh_token returns non-2xx. The refresh token was revoked or expired server-side, the client_id/client_secret changed, or the token was already rotated (one-time-use refresh tokens).","commonSituations":"Refresh token expired past its lifetime. User signed out / revoked access on another device, invalidating the family. Server rotated to a new refresh token on the last refresh and the old one is now dead. Client credentials changed.","solutions":["Treat this error as a re-authentication trigger: discard stored tokens and restart the authorization-code flow via buildAuthorizationURL().","If using rotated refresh tokens, ensure each refresh stores the new refresh_token from the response so the next refresh uses the live token.","Confirm client_id (and clientSecret for confidential clients) match the registered client.","If 400 invalid_grant recurs with fresh tokens, check server refresh-token revocation policy."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// before refreshing, confirm the token is present and not obviously expired\nif (!refreshToken) throw new Error('No refresh token stored')\n// let the refresh attempt proceed; the server is the source of truth","typeGuard":"null","tryCatchPattern":"try {\n  return await refreshAccessToken(clientId, refreshToken, clientSecret)\n}\ncatch (err) {\n  if (err instanceof Error && err.message.startsWith('Token refresh failed')) {\n    // refresh token unusable → clear tokens, force re-authentication via authorization code flow\n  }\n  else throw err\n}","preventionTips":["Store the new refresh_token from each refresh response (rotated tokens).","Treat refresh failure as a re-auth trigger, not a retry.","Keep client_id/secret consistent with the registered client."],"tags":["network","oidc","oauth","auth","token-refresh"],"backgroundTag":null,"analyzedSha":"27111382b4a79a7e983289d6e983a06af185ed0f","analyzedAt":"2026-08-12T18:33:34.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}