slymnoyann/hey-1 · critical · Error
Refresh token is invalid or expired
Error message
Refresh token is invalid or expired
What it means
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.
Source
Thrown at src/helpers/tokenManager.ts:42
if (refreshResult.__typename === "AuthenticationTokens") {
const { accessToken: newAccessToken, refreshToken: newRefreshToken } =
refreshResult;
if (!newAccessToken || !newRefreshToken) {
throw new Error("Missing tokens in refresh response");
}
signIn({
accessToken: newAccessToken,
refreshToken: newRefreshToken
});
return newAccessToken;
}
if (refreshResult.__typename === "ForbiddenError") {
signOut();
throw new Error("Refresh token is invalid or expired");
}
if (attempt < MAX_RETRIES - 1) {
await new Promise((resolve) =>
setTimeout(resolve, 2 ** attempt * 1000)
);
}
}
throw new Error("Unknown error during token refresh");
} finally {
refreshPromise = null;
}
};
export const refreshTokens = (refreshToken: string): Promise<string> => {
if (!refreshPromise) {
refreshPromise = executeTokenRefresh(refreshToken);View on GitHub (pinned to 88c8f9d553)
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
Example fix
// before
if (refreshResult.__typename === "ForbiddenError") {
signOut();
throw new Error("Refresh token is invalid or expired");
}
// after (caller side)
try {
await refreshTokens(refreshToken);
} catch (e) {
if (e instanceof Error && e.message.includes("invalid or expired")) {
window.location.href = "/login?reason=session_expired";
}
throw e;
} Defensive patterns
Strategy: try-catch
Validate before calling
try {
const decoded = JSON.parse(atob(refreshToken.split(".")[1]));
if (decoded.exp && decoded.exp * 1000 < Date.now()) {
signOut(); // proactively expire the session
}
} catch { /* opaque token; let the server decide */ } Type guard
const isForbiddenRefresh = (r: unknown): r is { __typename: "ForbiddenError" } =>
typeof r === "object" && r !== null && (r as { __typename?: unknown }).__typename === "ForbiddenError"; Try / catch
try {
await refreshTokens(refreshToken);
} catch (e) {
if (e instanceof Error && e.message === "Refresh token is invalid or expired") {
window.location.href = "/login?reason=session_expired";
return;
}
throw e;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
AI-assisted analysis of slymnoyann/hey-1@88c8f9d553 (2026-08-28).
Data as JSON: /api/errors/2ddb42027efdb375.
Report an issue: GitHub.