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

  1. Always pass `state` with `onErrorReturnTo`/`returnTo` when starting OAuth so a missing code redirects gracefully instead of throwing.
  2. Treat a missing `code` alongside `?error=...` as a user-cancelled flow and surface a friendly 'permissions denied' message rather than a 400.
  3. 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

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


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