{"record":{"id":"2ddb42027efdb375","repo":"slymnoyann/hey-1","slug":"refresh-token-is-invalid-or-expired","errorCode":null,"errorMessage":"Refresh token is invalid or expired","messagePattern":"Refresh token is invalid or expired","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"src/helpers/tokenManager.ts","lineNumber":42,"sourceCode":"      if (refreshResult.__typename === \"AuthenticationTokens\") {\n        const { accessToken: newAccessToken, refreshToken: newRefreshToken } =\n          refreshResult;\n\n        if (!newAccessToken || !newRefreshToken) {\n          throw new Error(\"Missing tokens in refresh response\");\n        }\n\n        signIn({\n          accessToken: newAccessToken,\n          refreshToken: newRefreshToken\n        });\n\n        return newAccessToken;\n      }\n\n      if (refreshResult.__typename === \"ForbiddenError\") {\n        signOut();\n        throw new Error(\"Refresh token is invalid or expired\");\n      }\n\n      if (attempt < MAX_RETRIES - 1) {\n        await new Promise((resolve) =>\n          setTimeout(resolve, 2 ** attempt * 1000)\n        );\n      }\n    }\n\n    throw new Error(\"Unknown error during token refresh\");\n  } finally {\n    refreshPromise = null;\n  }\n};\n\nexport const refreshTokens = (refreshToken: string): Promise<string> => {\n  if (!refreshPromise) {\n    refreshPromise = executeTokenRefresh(refreshToken);","sourceCodeStart":24,"sourceCodeEnd":60,"githubUrl":"https://github.com/slymnoyann/hey-1/blob/88c8f9d55340d57a37846ff72f55c2c604e3a566/src/helpers/tokenManager.ts#L24-L60","documentation":"Thrown when the refresh mutation returns a ForbiddenError typename, meaning the server explicitly rejected the refresh token as invalid or expired. Unlike unknown failures, this is a definitive verdict: the session cannot be renewed, so signOut() is called and the error propagates to terminate the auth flow.","triggerScenarios":"Refreshing with a refresh token past its server-side expiry, a token revoked by logout-all-devices or password change, a token signed with a stale secret after backend rotation, or a token belonging to a deleted account.","commonSituations":"User returning to the app after a long absence with an expired persisted token, token stored under a different environment (staging token sent to prod), JWT secret rotation on the server invalidating old tokens, or the same account being used after credentials changed.","solutions":["Ensure signOut() fully clears persisted tokens so the next load starts clean","Redirect the user to the login page when this error is caught, rather than showing a generic error","If tokens expire too quickly for your use case, extend refresh token TTL server-side or implement silent refresh before expiry","Check for environment mismatch (staging token against production API) if expiry seems premature"],"exampleFix":"// before\nif (refreshResult.__typename === \"ForbiddenError\") {\n  signOut();\n  throw new Error(\"Refresh token is invalid or expired\");\n}\n\n// after (caller side)\ntry {\n  await refreshTokens(refreshToken);\n} catch (e) {\n  if (e instanceof Error && e.message.includes(\"invalid or expired\")) {\n    window.location.href = \"/login?reason=session_expired\";\n  }\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"try {\n  const decoded = JSON.parse(atob(refreshToken.split(\".\")[1]));\n  if (decoded.exp && decoded.exp * 1000 < Date.now()) {\n    signOut(); // proactively expire the session\n  }\n} catch { /* opaque token; let the server decide */ }","typeGuard":"const isForbiddenRefresh = (r: unknown): r is { __typename: \"ForbiddenError\" } =>\n  typeof r === \"object\" && r !== null && (r as { __typename?: unknown }).__typename === \"ForbiddenError\";","tryCatchPattern":"try {\n  await refreshTokens(refreshToken);\n} catch (e) {\n  if (e instanceof Error && e.message === \"Refresh token is invalid or expired\") {\n    window.location.href = \"/login?reason=session_expired\";\n    return;\n  }\n  throw e;\n}","preventionTips":["Refresh tokens silently before they expire instead of on failure","Clear all auth storage in signOut() to avoid replaying dead tokens","Avoid mixing tokens between staging and production environments"],"tags":["authentication","session-expired","forbidden","token-refresh"],"backgroundTag":"refresh-token-expired","analyzedSha":"88c8f9d55340d57a37846ff72f55c2c604e3a566","analyzedAt":"2026-08-28T16:20:43.640Z","schemaVersion":2},"datasetVersion":"2026-08-28T21:17:43.275Z"}