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
- Inspect the forwarded error string: 'invalid_grant' means reuse/expiry of code (re-auth), 'invalid_client' means bad secret/id.
- Regenerate the auth URL and complete the flow in one shot without reusing the code.
- 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
- Use each authorization code exactly once and immediately after issuance.
- Register the exact redirect_uri in Azure and reuse it verbatim.
- Rotate client secrets on both Azure and the app in lockstep.
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
- ${responseBody.error}
- Missing `state` query param
- {error_description}
- Invalid conferencing app, available apps are:
- Something is wrong with Zoom API
AI-assisted analysis of calcom/cal.diy@176037d0af (2026-08-12).
Data as JSON: /api/errors/02d523dda7ade8d2.
Report an issue: GitHub.