{"record":{"id":"2471137abf094737","repo":"toeverything/AFFiNE","slug":"invalid-oauth-callback-code","errorCode":"invalid_oauth_callback_code","errorMessage":"Invalid callback code parameter, provider response status: ${status} and body: ${body}.","messagePattern":"Invalid callback code parameter, provider response status: (.+?) and body: (.+?)\\.","errorType":"exception","errorClass":"InvalidOauthCallbackCode","httpStatus":400,"severity":"error","filePath":"packages/backend/server/src/plugins/oauth/providers/def.ts","lineNumber":113,"sourceCode":"    options?: { treatServerErrorAsInvalid?: boolean }\n  ) {\n    const response = await safeFetch(\n      url,\n      {\n        ...init,\n        headers: {\n          ...init?.headers,\n          Accept: 'application/json',\n          'User-Agent': 'AFFiNE-Server',\n        },\n      },\n      this.fetchOptions(url)\n    );\n\n    const body = await response.text();\n    if (!response.ok) {\n      if (response.status < 500 || options?.treatServerErrorAsInvalid) {\n        throw new InvalidOauthCallbackCode({ status: response.status, body });\n      }\n      throw new Error(\n        `Server responded with non-success status ${response.status}, body: ${body}`\n      );\n    }\n\n    if (!body) {\n      return {} as T;\n    }\n\n    try {\n      return JSON.parse(body) as T;\n    } catch {\n      throw new InvalidOauthResponse({\n        reason: `Unable to parse JSON response from ${url}`,\n      });\n    }\n  }","sourceCodeStart":95,"sourceCodeEnd":131,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b6de0ad51b76f3daac2d3d6325369ea623ed7ed4/packages/backend/server/src/plugins/oauth/providers/def.ts#L95-L131","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"try {\n  await client.completeOAuthFlow(code, state);\n} catch (e) {\n  if (e.code === 'invalid_oauth_callback_code') {\n    if (e.args.status >= 500) return retryLaterWithFreshFlow(); // provider outage\n    if (/invalid_grant/.test(e.args.body)) return restartFromPreflight(); // expired/used code\n    if (/invalid_client/.test(e.args.body)) throw new Error('provider credentials misconfigured');\n  }\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"const iss = new URL(authorizeUrl).origin;\nconst probe = await fetch(iss, { method: 'HEAD' }).catch(() => null);\nif (probe && probe.status >= 500) {\n  deferLoginRetry(); // provider outage: don't burn a fresh code now\n}","typeGuard":"interface OauthCallbackCodeError {\n  code: 'invalid_oauth_callback_code';\n  args: { status: number; body: string };\n}\nfunction isCallbackCodeError(e: unknown): e is OauthCallbackCodeError {\n  return typeof e === 'object' && e !== null && (e as any).code === 'invalid_oauth_callback_code';\n}","tryCatchPattern":"try {\n  await exchangeCode(code, state);\n} catch (e) {\n  if (isCallbackCodeError(e)) {\n    if (e.args.status >= 500) return scheduleFlowRestart('provider outage');\n    if (/invalid_grant/.test(e.args.body)) return restartFromPreflight(); // expired/used code\n    if (/invalid_client/.test(e.args.body)) return alertAdmin('OAuth client credentials rejected');\n  }\n  throw e;\n}","preventionTips":["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"],"tags":["oauth","token-exchange","provider-error","http-status"],"backgroundTag":"oauth-code-exchange-failed","analyzedSha":"b6de0ad51b76f3daac2d3d6325369ea623ed7ed4","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}