toeverything/AFFiNE · warning · AuthSessionHttpError

access_token_expired

access_token_expired

Error message

The access token has expired.

What it means

Surfaced when `AccessTokenService.verify` throws `SessionAccessTokenError('ACCESS_TOKEN_EXPIRED')`: the token's `exp` claim is in the past relative to the server clock. Re-wrapped as `AuthSessionHttpError` with HTTP 401. The auth session itself may still be valid; only the short-lived access token expired.

Source

Thrown at packages/backend/server/src/core/auth/guard.ts:144

    const result = await this.resolveRequestSession(req, res, isPublic);
    return result?.session ?? null;
  }

  private async resolveRequestSession(
    req: Request,
    res?: Response,
    isPublic = false
  ): Promise<AuthenticatedRequestSession | null> {
    const bearer = req.headers.authorization
      ? extractTokenFromHeader(req.headers.authorization)
      : undefined;
    if (bearer && isLikelyJwt(bearer)) {
      try {
        const session = await this.signInWithJwt(req, bearer, res, isPublic);
        return session ? { type: 'jwt', session } : null;
      } catch (err) {
        if (err instanceof SessionAccessTokenError) {
          throw new AuthSessionHttpError(err.code);
        }
        throw err;
      }
    }

    const session = await this.signInWithCookie(req, res, isPublic);
    return session ? { type: 'cookie_session', session } : null;
  }

  async signInWithJwt(
    req: Request,
    token: string,
    res?: Response,
    isPublic = false
  ): Promise<Session | null> {
    if (req.session && req.authType === 'jwt') return req.session;
    const session = await this.accessTokens.verify(token);
    const versionAllowed = await this.checkUserSessionClientVersion(

View on GitHub (pinned to 26c515e050)

Solutions

  1. Call `POST /api/auth/session/refresh` with the refresh token to obtain a new access token, then retry.
  2. Implement proactive refresh before the access token's `expiresIn` elapses.
  3. Handle 401 access_token_expired by transparently refreshing once and replaying the request.

Example fix

// before
fetch(url, { headers: { authorization: `Bearer ${expiredToken}` } }); // 401

// after
if (tokenExpired(accessToken)) {
  const refreshed = await refreshSession(refreshToken);
  accessToken = refreshed.accessToken;
}
fetch(url, { headers: { authorization: `Bearer ${accessToken}` } });
Defensive patterns

Strategy: retry

Validate before calling

function tokenExpiresIn(token: string, now = Date.now()): number {
  const p = JSON.parse(atob(token.split('.')[1]));
  return (p.exp * 1000) - now;
}
if (tokenExpiresIn(accessToken) < 30_000) {
  ({ accessToken } = await refreshSession(refreshToken));
}

Try / catch

async function fetchWithRefresh(url: string, init: RequestInit): Promise<Response> {
  let res = await fetch(url, { ...init, headers: { ...init.headers, authorization: `Bearer ${accessToken}` } });
  if (res.status === 401) {
    const body = await res.clone().json().catch(() => ({}));
    if (body.code === 'access_token_expired') {
      ({ accessToken } = await refreshSession(refreshToken));
      res = await fetch(url, { ...init, headers: { ...init.headers, authorization: `Bearer ${accessToken}` } });
    }
  }
  return res;
}

Prevention

When it happens

Trigger: Using an access token past its `accessTokenTtl` lifetime without refreshing, or a client clock that minted/kept a token whose expiry already passed.

Common situations: Long-running client sessions where the access token was never refreshed, background tabs waking after sleep with a stale token, or clock drift between client and server.

Related errors


AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12). Data as JSON: /api/errors/16b11d4233dd7dcb. Report an issue: GitHub.