calcom/cal.diy · warning · HttpError
`code` must be a string
Error message
`code` must be a string
What it means
OAuth callback guard. Google redirects to `/api/integrations/googlecalendar/callback?code=...`; if `code` is missing or an array (duplicate query param), this throws `HttpError` **400** — unless `state.onErrorReturnTo` or `state.returnTo` is set, in which case it redirects gracefully and returns. Most commonly means Google sent no `code` because the user denied consent (Google then sends `error`/`error_description` instead).
Source
Thrown at packages/app-store/googlecalendar/api/callback.ts:39
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
import { updateProfilePhotoGoogle } from "../../_utils/oauth/updateProfilePhotoGoogle";
import { getGoogleAppKeys } from "../lib/getGoogleAppKeys";
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
const { code } = req.query;
const state = decodeOAuthState(req);
if (typeof code !== "string") {
if (state?.onErrorReturnTo || state?.returnTo) {
res.redirect(
getSafeRedirectUrl(state.onErrorReturnTo) ??
getSafeRedirectUrl(state?.returnTo) ??
`${WEBAPP_URL}/apps/installed`
);
return;
}
throw new HttpError({ statusCode: 400, message: "`code` must be a string" });
}
if (!req.session?.user?.id) {
throw new HttpError({ statusCode: 401, message: "You must be logged in to do this" });
}
const { client_id, client_secret } = await getGoogleAppKeys();
const redirect_uri = `${WEBAPP_URL_FOR_OAUTH}/api/integrations/googlecalendar/callback`;
const oAuth2Client = new OAuth2Client(client_id, client_secret, redirect_uri);
if (code) {
const token = await oAuth2Client.getToken(code);
const key = token.tokens;
const grantedScopes = token.tokens.scope?.split(" ") ?? [];
// Check if we have granted all required permissions
const hasMissingRequiredScopes = GOOGLE_CALENDAR_SCOPES.some((scope) => !grantedScopes.includes(scope));View on GitHub (pinned to 176037d0af)
Solutions
- Always pass `state` with `onErrorReturnTo`/`returnTo` when starting OAuth so a missing code redirects gracefully instead of throwing.
- Treat a missing `code` alongside `?error=...` as a user-cancelled flow and surface a friendly 'permissions denied' message rather than a 400.
- Confirm the `redirect_uri` registered in Google Cloud Console matches `${WEBAPP_URL_FOR_OAUTH}/api/integrations/googlecalendar/callback` exactly.
Example fix
// before
if (typeof code !== "string") {
if (state?.onErrorReturnTo || state?.returnTo) { res.redirect(...); return; }
throw new HttpError({ statusCode: 400, message: "`code` must be a string" });
}
// after - distinguish explicit Google denial from a malformed request
if (typeof code !== "string") {
if (state?.onErrorReturnTo || state?.returnTo) { res.redirect(...); return; }
const denied = "error" in req.query;
throw new HttpError({
statusCode: 400,
message: denied ? "Google authorization was denied" : "`code` must be a string",
});
} Defensive patterns
Strategy: validation
Validate before calling
// Before processing the callback, validate the query shape and detect explicit denial
function readOAuthCode(query: NodeJS.Query): string | null {
const c = query.code;
if (typeof c === "string" && c.length > 0) return c;
return null; // missing or duplicated — caller should treat as denial/malformed
}
const code = readOAuthCode(req.query as NodeJS.Query);
if (!code && !("error" in req.query)) {
// genuinely malformed request, not a user denial
} Type guard
function isStringCode(value: unknown): value is string {
return typeof value === "string" && value.length > 0;
} Prevention
- Always start OAuth with state containing onErrorReturnTo/returnTo so a missing code redirects instead of throwing.
- When code is absent, check for Google's `error` param and treat it as user-cancelled, not a hard error.
- Keep the registered redirect_uri in Google Cloud byte-for-byte identical to the callback URL.
When it happens
Trigger: User clicked Cancel/Deny on Google's consent screen (no `code`, possibly `?error=access_denied`); Google redirected with `code` duplicated into an array; the callback URL was hit manually with no code; a Google-side error aborted the grant before issuing a code.
Common situations: User denied calendar permissions; misconfigured `redirect_uri` in Google Cloud causing Google to drop the code; proxy/load-balancer rewriting query strings; manual URL testing without a real code.
Related errors
- You must grant all permissions to use this integration
- You must be logged in to do this
- Session user must have an email
- You must be logged in to do this
- Missing `state` query param
AI-assisted analysis of calcom/cal.diy@176037d0af (2026-08-12).
Data as JSON: /api/errors/c779fa136822a9b3.
Report an issue: GitHub.