moeru-ai/airi · error · Error

Token refresh failed: ${response.status}

Error message

Token refresh failed: ${response.status}

What it means

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.

Source

Thrown at packages/stage-ui/src/libs/auth-oidc.ts:140

    grant_type: 'refresh_token',
    refresh_token: refreshToken,
    client_id: clientId,
    resource: SERVER_URL,
  }

  if (clientSecret)
    params.client_secret = clientSecret

  const body = new URLSearchParams(params)

  const response = await fetch(new URL(OIDC_TOKEN_PATH, SERVER_URL), {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body,
  })

  if (!response.ok)
    throw new Error(`Token refresh failed: ${response.status}`)

  return await response.json()
}

// Session storage keys for PKCE flow state (survives page navigation during OAuth)
const FLOW_STATE_KEY = 'auth/v1/oidc-flow-state'
const FLOW_PARAMS_KEY = 'auth/v1/oidc-flow-params'

/**
 * Persist OIDC flow state before navigating to the authorization server.
 */
export function persistFlowState(flowState: OIDCFlowState, params: OIDCFlowParams): void {
  sessionStorage.setItem(FLOW_STATE_KEY, JSON.stringify(flowState))
  sessionStorage.setItem(FLOW_PARAMS_KEY, JSON.stringify(params))
}

/**
 * Retrieve and clear persisted OIDC flow state after callback.

View on GitHub (pinned to 27111382b4)

Solutions

  1. Treat this error as a re-authentication trigger: discard stored tokens and restart the authorization-code flow via buildAuthorizationURL().
  2. If using rotated refresh tokens, ensure each refresh stores the new refresh_token from the response so the next refresh uses the live token.
  3. Confirm client_id (and clientSecret for confidential clients) match the registered client.
  4. If 400 invalid_grant recurs with fresh tokens, check server refresh-token revocation policy.
Defensive patterns

Strategy: try-catch

Validate before calling

// before refreshing, confirm the token is present and not obviously expired
if (!refreshToken) throw new Error('No refresh token stored')
// let the refresh attempt proceed; the server is the source of truth

Type guard

null

Try / catch

try {
  return await refreshAccessToken(clientId, refreshToken, clientSecret)
}
catch (err) {
  if (err instanceof Error && err.message.startsWith('Token refresh failed')) {
    // refresh token unusable → clear tokens, force re-authentication via authorization code flow
  }
  else throw err
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of moeru-ai/airi@27111382b4 (2026-08-12). Data as JSON: /api/errors/125978890338f9a9. Report an issue: GitHub.