{"record":{"id":"2d305cf4a3d467ed","repo":"calcom/cal.diy","slug":"reason-err-message","errorCode":null,"errorMessage":"${reason ?? err.message}","messagePattern":"\\$\\{reason \\?\\? err\\.message\\}","errorType":"http","errorClass":"OAuth2HttpException","httpStatus":null,"severity":"error","filePath":"apps/api/v2/src/modules/auth/oauth2/services/oauth2-error.service.ts","lineNumber":44,"sourceCode":"        throw new OAuth2HttpException(\n          {\n            error: err.message,\n            error_description: reason,\n          },\n          statusCode\n        );\n      }\n    }\n\n    const errorRedirectUrl = this.oAuthService.buildErrorRedirectUrl(redirectUri, err, state);\n    throw new OAuth2RedirectException(errorRedirectUrl);\n  }\n\n  handleTokenError(err: unknown): never {\n    if (err instanceof ErrorWithCode) {\n      const statusCode = getHttpStatusCode(err);\n      const reason = err.data?.[\"reason\"] as string | undefined;\n      throw new OAuth2HttpException(\n        {\n          error: err.message,\n          error_description: reason ?? err.message,\n        },\n        statusCode\n      );\n    }\n    this.logger.error(err);\n    throw new OAuth2HttpException(\n      {\n        error: \"server_error\",\n        error_description: \"An unexpected error occurred\",\n      },\n      500\n    );\n  }\n\n  handleClientError(err: unknown, fallbackMessage: string): never {","sourceCodeStart":26,"sourceCodeEnd":62,"githubUrl":"https://github.com/calcom/cal.diy/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/modules/auth/oauth2/services/oauth2-error.service.ts#L26-L62","documentation":"Thrown by handleTokenError when the token-exchange error is an ErrorWithCode. The OAuth2 token endpoint (/v2/oauth/{clientId}/token) returns a JSON body {error: err.message, error_description: reason ?? err.message} with the HTTP status derived from the error code via getHttpStatusCode. error_description falls back to err.message when no reason is present in err.data.","triggerScenarios":"POST to /v2/oauth/{clientId}/token with an invalid or expired authorization code, an invalid grant_type, a missing or wrong code_verifier for PKCE, a client_secret mismatch, or an already-consumed code. Any ErrorWithCode raised by the grant handling surfaces here.","commonSituations":"Reusing an authorization code after it was already exchanged (codes are single-use); clock drift causing the code to appear expired; PKCE verifier mismatch because the S256 challenge was generated with a different secret; wrong client_secret copied from another client.","solutions":["Read the error field in the JSON response — it is the OAuth2 error code (e.g., invalid_grant, invalid_client) that pinpoints the failure.","If invalid_grant: generate a fresh authorization code via the /authorize flow rather than retrying the same code.","If invalid_client: verify client_id and client_secret against the platform settings page for the correct environment.","For PKCE flows, ensure the code_verifier sent to /token matches the code_challenge sent to /authorize (both S256 and the same random string)."],"exampleFix":"// before — reusing a code after a failed attempt\nconst tokenRes = await fetch(tokenUrl, { method: 'POST', body: new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri, client_id, client_secret }) });\n// retry with the same `code` → invalid_grant\n\n// after — on failure, restart the authorize flow to get a new code\nif (!tokenRes.ok) { window.location.href = authorizeUrl; return; }","handlingStrategy":"try-catch","validationCode":"// Validate PKCE verifier against the stored challenge before calling /token\nfunction validatePkce(verifier: string, challenge: string) {\n  if (!verifier || verifier.length < 43) throw new Error('code_verifier too short');\n  const expected = base64url(sha256(verifier));\n  if (expected !== challenge) throw new Error('PKCE verifier/challenge mismatch');\n}\n// Validate grant_type is one of the supported values\nconst GRANT_TYPES = new Set(['authorization_code','refresh_token','client_credentials']);\nif (!GRANT_TYPES.has(grantType)) throw new Error(`unsupported grant_type: ${grantType}`);","typeGuard":"interface OAuth2TokenErrorBody { error: string; error_description?: string; }\nfunction isOAuth2TokenErrorBody(v: unknown): v is OAuth2TokenErrorBody {\n  return typeof v === 'object' && v !== null && typeof (v as any).error === 'string';\n}","tryCatchPattern":"try {\n  const token = await exchangeCodeForToken(code, verifier);\n} catch (err) {\n  if (err.response && isOAuth2TokenErrorBody(err.response.data)) {\n    const { error, error_description } = err.response.data;\n    if (error === 'invalid_grant') { redirectToAuthorize(); return; }\n    if (error === 'invalid_client') { refreshClientSecret(); return; }\n  }\n  throw err;\n}","preventionTips":["Treat authorization codes as single-use; never replay a code after any outcome.","Store the PKCE verifier alongside the state so the /token call always has the matching pair.","Map known error codes (invalid_grant, invalid_client) to specific recovery flows rather than generic retry."],"tags":["oauth2","token","authentication","invalid-grant","pkce"],"backgroundTag":null,"analyzedSha":"176037d0afbe572f870a3c702985e7cd83fe6c0c","analyzedAt":"2026-08-12T19:12:41.464Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}