toeverything/AFFiNE · error · InvalidOauthCallbackCode

invalid_oauth_callback_code

invalid_oauth_callback_code

Error message

Invalid callback code parameter, provider response status: ${status} and body: ${body}.

What it means

Base OAuth provider fetchJson helper: the provider's token/userinfo endpoint answered HTTP non-OK. If status < 500 (or the caller passed treatServerErrorAsInvalid, as the OIDC token exchange does), the status and body are wrapped into InvalidOauthCallbackCode. The body carries the provider's OAuth error (e.g. invalid_grant, invalid_client), making this the generic 'code exchange rejected' error.

Source

Thrown at packages/backend/server/src/plugins/oauth/providers/def.ts:113

    options?: { treatServerErrorAsInvalid?: boolean }
  ) {
    const response = await safeFetch(
      url,
      {
        ...init,
        headers: {
          ...init?.headers,
          Accept: 'application/json',
          'User-Agent': 'AFFiNE-Server',
        },
      },
      this.fetchOptions(url)
    );

    const body = await response.text();
    if (!response.ok) {
      if (response.status < 500 || options?.treatServerErrorAsInvalid) {
        throw new InvalidOauthCallbackCode({ status: response.status, body });
      }
      throw new Error(
        `Server responded with non-success status ${response.status}, body: ${body}`
      );
    }

    if (!body) {
      return {} as T;
    }

    try {
      return JSON.parse(body) as T;
    } catch {
      throw new InvalidOauthResponse({
        reason: `Unable to parse JSON response from ${url}`,
      });
    }
  }

View on GitHub (pinned to b6de0ad51b)

Solutions

  1. Read the error's status and body fields: invalid_grant means restart a fresh flow from preflight; invalid_client means fix credentials
  2. If status ≥ 500 and body looks like an outage page, wait and retry the complete flow — the code itself may be fine but time-limited, so restart from preflight after the provider recovers
  3. For Apple, confirm the client_secret JWT (ES-signed with the private key, ~6-month cap) is still valid and the key ID/team ID are correct
  4. Verify redirect_uri used at the token endpoint matches the one from the authorize request

Example fix

try {
  await client.completeOAuthFlow(code, state);
} catch (e) {
  if (e.code === 'invalid_oauth_callback_code') {
    if (e.args.status >= 500) return retryLaterWithFreshFlow(); // provider outage
    if (/invalid_grant/.test(e.args.body)) return restartFromPreflight(); // expired/used code
    if (/invalid_client/.test(e.args.body)) throw new Error('provider credentials misconfigured');
  }
  throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

const iss = new URL(authorizeUrl).origin;
const probe = await fetch(iss, { method: 'HEAD' }).catch(() => null);
if (probe && probe.status >= 500) {
  deferLoginRetry(); // provider outage: don't burn a fresh code now
}

Type guard

interface OauthCallbackCodeError {
  code: 'invalid_oauth_callback_code';
  args: { status: number; body: string };
}
function isCallbackCodeError(e: unknown): e is OauthCallbackCodeError {
  return typeof e === 'object' && e !== null && (e as any).code === 'invalid_oauth_callback_code';
}

Try / catch

try {
  await exchangeCode(code, state);
} catch (e) {
  if (isCallbackCodeError(e)) {
    if (e.args.status >= 500) return scheduleFlowRestart('provider outage');
    if (/invalid_grant/.test(e.args.body)) return restartFromPreflight(); // expired/used code
    if (/invalid_client/.test(e.args.body)) return alertAdmin('OAuth client credentials rejected');
  }
  throw e;
}

Prevention

When it happens

Trigger: Exchanging an expired or already-used authorization code (400 invalid_grant); wrong client secret or expired Apple client_secret JWT (401 invalid_client); redirect_uri mismatch at the token endpoint; provider 5xx outage when treatServerErrorAsInvalid was set (OIDC token endpoint, Apple JWKS fetch).

Common situations: User sat on the consent page until the code expired; double callback firing (retry logic or prefetch) consuming the code once; rotated client secret not deployed; transient IdP outage surfacing as a bad-code error.

Related errors


AI-assisted analysis of toeverything/AFFiNE@b6de0ad51b (2026-08-18). Data as JSON: /api/errors/2471137abf094737. Report an issue: GitHub.