calcom/cal.diy · error · OAuth2HttpException

${reason ?? err.message}

Error message

${reason ?? err.message}

What it means

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.

Source

Thrown at apps/api/v2/src/modules/auth/oauth2/services/oauth2-error.service.ts:44

        throw new OAuth2HttpException(
          {
            error: err.message,
            error_description: reason,
          },
          statusCode
        );
      }
    }

    const errorRedirectUrl = this.oAuthService.buildErrorRedirectUrl(redirectUri, err, state);
    throw new OAuth2RedirectException(errorRedirectUrl);
  }

  handleTokenError(err: unknown): never {
    if (err instanceof ErrorWithCode) {
      const statusCode = getHttpStatusCode(err);
      const reason = err.data?.["reason"] as string | undefined;
      throw new OAuth2HttpException(
        {
          error: err.message,
          error_description: reason ?? err.message,
        },
        statusCode
      );
    }
    this.logger.error(err);
    throw new OAuth2HttpException(
      {
        error: "server_error",
        error_description: "An unexpected error occurred",
      },
      500
    );
  }

  handleClientError(err: unknown, fallbackMessage: string): never {

View on GitHub (pinned to 176037d0af)

Solutions

  1. Read the error field in the JSON response — it is the OAuth2 error code (e.g., invalid_grant, invalid_client) that pinpoints the failure.
  2. If invalid_grant: generate a fresh authorization code via the /authorize flow rather than retrying the same code.
  3. If invalid_client: verify client_id and client_secret against the platform settings page for the correct environment.
  4. For PKCE flows, ensure the code_verifier sent to /token matches the code_challenge sent to /authorize (both S256 and the same random string).

Example fix

// before — reusing a code after a failed attempt
const tokenRes = await fetch(tokenUrl, { method: 'POST', body: new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri, client_id, client_secret }) });
// retry with the same `code` → invalid_grant

// after — on failure, restart the authorize flow to get a new code
if (!tokenRes.ok) { window.location.href = authorizeUrl; return; }
Defensive patterns

Strategy: try-catch

Validate before calling

// Validate PKCE verifier against the stored challenge before calling /token
function validatePkce(verifier: string, challenge: string) {
  if (!verifier || verifier.length < 43) throw new Error('code_verifier too short');
  const expected = base64url(sha256(verifier));
  if (expected !== challenge) throw new Error('PKCE verifier/challenge mismatch');
}
// Validate grant_type is one of the supported values
const GRANT_TYPES = new Set(['authorization_code','refresh_token','client_credentials']);
if (!GRANT_TYPES.has(grantType)) throw new Error(`unsupported grant_type: ${grantType}`);

Type guard

interface OAuth2TokenErrorBody { error: string; error_description?: string; }
function isOAuth2TokenErrorBody(v: unknown): v is OAuth2TokenErrorBody {
  return typeof v === 'object' && v !== null && typeof (v as any).error === 'string';
}

Try / catch

try {
  const token = await exchangeCodeForToken(code, verifier);
} catch (err) {
  if (err.response && isOAuth2TokenErrorBody(err.response.data)) {
    const { error, error_description } = err.response.data;
    if (error === 'invalid_grant') { redirectToAuthorize(); return; }
    if (error === 'invalid_client') { refreshClientSecret(); return; }
  }
  throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of calcom/cal.diy@176037d0af (2026-08-12). Data as JSON: /api/errors/2d305cf4a3d467ed. Report an issue: GitHub.