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
- Read the error's status and body fields: invalid_grant means restart a fresh flow from preflight; invalid_client means fix credentials
- 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
- 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
- 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
- Use each authorization code exactly once; disable retry/prefetch middleware on the callback route
- Keep provider client secrets automated (rotation) and Apple's 6-month client_secret JWT on a refresh schedule
- Match redirect_uri between authorize and token calls character-for-character
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
- invalid_oauth_response
- wrong_sign_in_method
- missing_oauth_query_parameter
- unknown_oauth_provider
- oauth_state_expired
AI-assisted analysis of toeverything/AFFiNE@b6de0ad51b (2026-08-18).
Data as JSON: /api/errors/2471137abf094737.
Report an issue: GitHub.