calcom/cal.diy · error · BadRequestException

{responseBody.error}

Error message

{responseBody.error}

What it means

Thrown after POSTing the authorization code to https://login.microsoftonline.com/common/oauth2/v2.0/token when response.ok is false. The thrown value is responseBody.error — the raw error object/string Azure returns (e.g. invalid_grant, invalid_client). It is passed verbatim to BadRequestException (HTTP 400), so the surfaced message is whatever Microsoft sent, not a fixed string.

Source

Thrown at apps/api/v2/src/modules/conferencing/services/office365-video.service.ts:92

      grant_type: "authorization_code",
      code,
      scope: this.scopes.join(" "),
      redirect_uri: this.redirectUri,
      client_secret,
    });

    const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
      method: "POST",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
      },
      body,
    });

    const responseBody = await response.json();

    if (!response.ok) {
      throw new BadRequestException(responseBody.error);
    }

    const whoami = await fetch("https://graph.microsoft.com/v1.0/me", {
      headers: { Authorization: `Bearer ${responseBody.access_token}` },
    });

    const graphUser = await whoami.json();

    // In some cases, graphUser.mail is null. Then graphUser.userPrincipalName most likely contains the email address.
    responseBody.email = graphUser.mail ?? graphUser.userPrincipalName;
    responseBody.expiry_date = Math.round(+new Date() / 1000 + responseBody.expires_in); // set expiry date in seconds
    delete responseBody.expires_in;

    const existingCredentialOffice365Video = teamId
      ? await this.credentialsRepository.findAllCredentialsByTypeAndTeamId(OFFICE_365_VIDEO_TYPE, teamId)
      : await this.credentialsRepository.findAllCredentialsByTypeAndUserId(OFFICE_365_VIDEO_TYPE, userId);

    const credentialIdsToDelete = existingCredentialOffice365Video.map((item) => item.id);

View on GitHub (pinned to 176037d0af)

Solutions

  1. Inspect the forwarded error string: 'invalid_grant' means reuse/expiry of code (re-auth), 'invalid_client' means bad secret/id.
  2. Regenerate the auth URL and complete the flow in one shot without reusing the code.
  3. Confirm the redirect_uri in the token request matches the Azure app registration exactly (scheme, host, path, trailing slash).
Defensive patterns

Strategy: try-catch

Try / catch

try {
  await office365Service.connectOffice365App(state, code, userId, teamId);
} catch (e) {
  if (e instanceof BadRequestException && e.message) {
    const azureError = String(e.message);
    if (azureError.includes('invalid_grant')) {
      // re-initiate OAuth flow with a fresh code
    } else if (azureError.includes('invalid_client')) {
      // alert ops: client_id/secret mismatch
    }
  }
  throw e;
}

Prevention

When it happens

Trigger: connectOffice365App with an expired/reused authorization code, wrong redirect_uri, mismatched client_id/secret, or a user who denied consent. The token endpoint returns non-2xx and responseBody.error is forwarded.

Common situations: User pastes the OAuth URL or retries with an already-consumed code; redirect_uri in the request differs from the one registered in Azure; client_secret rotated in Azure but not in the app keys; clock skew or expired code window (codes are short-lived).

Related errors


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