{"record":{"id":"080c8aa64a97ac83","repo":"calcom/cal.diy","slug":"code-must-be-a-string","errorCode":null,"errorMessage":"`code` must be a string","messagePattern":"`code` must be a string","errorType":"http","errorClass":"HttpError","httpStatus":400,"severity":"warning","filePath":"packages/app-store/closecom/api/callback.ts","lineNumber":29,"sourceCode":"import getInstalledAppPath from \"../../_utils/getInstalledAppPath\";\nimport { decodeOAuthState } from \"../../_utils/oauth/decodeOAuthState\";\nimport appConfig from \"../config.json\";\n\nasync function getHandler(req: NextApiRequest, res: NextApiResponse) {\n  const { code } = req.query;\n\n  const state = decodeOAuthState(req);\n\n  const redirectAfterSuccess =\n    getSafeRedirectUrl(state?.returnTo) ??\n    getInstalledAppPath({ variant: appConfig.variant, slug: appConfig.slug });\n  const redirectAfterSuccessOrError = getSafeRedirectUrl(state?.onErrorReturnTo) ?? redirectAfterSuccess;\n  if (!code || typeof code !== \"string\") {\n    if (state?.onErrorReturnTo || state?.returnTo) {\n      res.redirect(redirectAfterSuccessOrError);\n      return;\n    }\n    throw new HttpError({ statusCode: 400, message: \"`code` must be a string\" });\n  }\n\n  if (!req.session?.user?.id) {\n    throw new HttpError({ statusCode: 401, message: \"You must be logged in to do this\" });\n  }\n\n  const { client_id, client_secret } = await getAppKeysFromSlug(\"closecom\");\n\n  if (!client_id || typeof client_id !== \"string\")\n    return res.status(400).json({ message: \"Close.com client_id missing.\" });\n  if (!client_secret || typeof client_secret !== \"string\")\n    return res.status(400).json({ message: \"Close.com client_secret missing.\" });\n\n  try {\n    const response = await fetch(\"https://api.close.com/oauth2/token/\", {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/x-www-form-urlencoded\",","sourceCodeStart":11,"sourceCodeEnd":47,"githubUrl":"https://github.com/calcom/cal.diy/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/app-store/closecom/api/callback.ts#L11-L47","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\nif (!code || typeof code !== \"string\") {\n  if (state?.onErrorReturnTo || state?.returnTo) {\n    res.redirect(redirectAfterSuccessOrError);\n    return;\n  }\n  throw new HttpError({ statusCode: 400, message: \"`code` must be a string\" });\n}\n\n// after\nif (Array.isArray(code)) code = code[0];\nif (!code || typeof code !== \"string\") {\n  if (state?.onErrorReturnTo || state?.returnTo) {\n    res.redirect(redirectAfterSuccessOrError);\n    return;\n  }\n  const reason = req.query.error ? `: ${req.query.error}` : \"\";\n  throw new HttpError({ statusCode: 400, message: `\\`code\\` must be a string${reason}` });\n}","handlingStrategy":"validation","validationCode":"if (Array.isArray(req.query.code)) req.query.code = req.query.code[0];\nconst code = req.query.code;\nif (!code || typeof code !== \"string\") {\n  if (state?.onErrorReturnTo || state?.returnTo) { res.redirect(redirectAfterSuccessOrError); return; }\n  return res.status(400).json({ message: `Missing code${req.query.error ? `: ${req.query.error}` : \"\"}` });\n}","typeGuard":"function isOAuthCode(v: unknown): v is string {\n  return typeof v === \"string\" && v.length > 0;\n}","tryCatchPattern":"try {\n  await handleCloseCallback(req, res);\n} catch (e) {\n  if (e instanceof HttpError && e.statusCode === 400 && /code/.test(e.message)) {\n    return res.redirect(`${state.onErrorReturnTo}?error=oauth_denied`);\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["oauth","callback","closecom","validation","authentication"],"backgroundTag":null,"analyzedSha":"176037d0afbe572f870a3c702985e7cd83fe6c0c","analyzedAt":"2026-08-12T19:12:41.464Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}