calcom/cal.diy · warning · HttpError

`code` must be a string

Error message

`code` must be a string

What it means

OAuth callback validation in the Close.com integration: `code` is read from the query, and if it is missing or not a string, HttpError 400 is thrown — unless an error/return redirect target exists in the OAuth state, in which case the user is redirected silently. The `code` is the authorization grant exchanged at Close's token endpoint.

Source

Thrown at packages/app-store/closecom/api/callback.ts:29

import getInstalledAppPath from "../../_utils/getInstalledAppPath";
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
import appConfig from "../config.json";

async function getHandler(req: NextApiRequest, res: NextApiResponse) {
  const { code } = req.query;

  const state = decodeOAuthState(req);

  const redirectAfterSuccess =
    getSafeRedirectUrl(state?.returnTo) ??
    getInstalledAppPath({ variant: appConfig.variant, slug: appConfig.slug });
  const redirectAfterSuccessOrError = getSafeRedirectUrl(state?.onErrorReturnTo) ?? redirectAfterSuccess;
  if (!code || typeof code !== "string") {
    if (state?.onErrorReturnTo || state?.returnTo) {
      res.redirect(redirectAfterSuccessOrError);
      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 getAppKeysFromSlug("closecom");

  if (!client_id || typeof client_id !== "string")
    return res.status(400).json({ message: "Close.com client_id missing." });
  if (!client_secret || typeof client_secret !== "string")
    return res.status(400).json({ message: "Close.com client_secret missing." });

  try {
    const response = await fetch("https://api.close.com/oauth2/token/", {
      method: "POST",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",

View on GitHub (pinned to 176037d0af)

Solutions

  1. Inspect the full callback query string for an `error`/`error_description` param and surface Close's reason.
  2. Verify the OAuth client's redirect URI in Close exactly matches `${WEBAPP_URL}/api/integrations/closecom/callback`.
  3. Always pass `onErrorReturnTo` in the OAuth state so failures redirect to a friendly page instead of throwing.
  4. Handle the array case (`Array.isArray(code)`) before the string check.

Example fix

// before
if (!code || typeof code !== "string") {
  if (state?.onErrorReturnTo || state?.returnTo) {
    res.redirect(redirectAfterSuccessOrError);
    return;
  }
  throw new HttpError({ statusCode: 400, message: "`code` must be a string" });
}

// after
if (Array.isArray(code)) code = code[0];
if (!code || typeof code !== "string") {
  if (state?.onErrorReturnTo || state?.returnTo) {
    res.redirect(redirectAfterSuccessOrError);
    return;
  }
  const reason = req.query.error ? `: ${req.query.error}` : "";
  throw new HttpError({ statusCode: 400, message: `\`code\` must be a string${reason}` });
}
Defensive patterns

Strategy: validation

Validate before calling

if (Array.isArray(req.query.code)) req.query.code = req.query.code[0];
const code = req.query.code;
if (!code || typeof code !== "string") {
  if (state?.onErrorReturnTo || state?.returnTo) { res.redirect(redirectAfterSuccessOrError); return; }
  return res.status(400).json({ message: `Missing code${req.query.error ? `: ${req.query.error}` : ""}` });
}

Type guard

function isOAuthCode(v: unknown): v is string {
  return typeof v === "string" && v.length > 0;
}

Try / catch

try {
  await handleCloseCallback(req, res);
} catch (e) {
  if (e instanceof HttpError && e.statusCode === 400 && /code/.test(e.message)) {
    return res.redirect(`${state.onErrorReturnTo}?error=oauth_denied`);
  }
  throw e;
}

Prevention

When it happens

Trigger: Close.com redirects back to `/api/integrations/closecom/callback` without a `code` query parameter — typically because the user denied consent, the request included an `error` param, the auth request was malformed, or `code` arrived as an array (duplicate query param).

Common situations: User clicks "Deny" on Close's consent screen; Close OAuth client misconfigured (wrong redirect URI) returning an error; a stale/ replayed link; duplicate `code=` params parsed by Next.js as an array.

Related errors


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