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
- Inspect the full callback query string for an `error`/`error_description` param and surface Close's reason.
- Verify the OAuth client's redirect URI in Close exactly matches `${WEBAPP_URL}/api/integrations/closecom/callback`.
- Always pass `onErrorReturnTo` in the OAuth state so failures redirect to a friendly page instead of throwing.
- 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
- Always pass `onErrorReturnTo` in the OAuth state.
- Verify the redirect URI matches exactly between authorize and token calls.
- Handle the `error` query param Close sends on denied consent.
- Dedupe/normalize the `code` query param before validation.
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
- No valid authentication method found
- `code` must be a string
- You must be logged in to do this
- Missing `state` query param
- {error_description}
AI-assisted analysis of calcom/cal.diy@176037d0af (2026-08-12).
Data as JSON: /api/errors/080c8aa64a97ac83.
Report an issue: GitHub.