{"record":{"id":"c779fa136822a9b3","repo":"calcom/cal.diy","slug":"code-must-be-a-string-c779fa","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/googlecalendar/api/callback.ts","lineNumber":39,"sourceCode":"import getInstalledAppPath from \"../../_utils/getInstalledAppPath\";\nimport { decodeOAuthState } from \"../../_utils/oauth/decodeOAuthState\";\nimport { updateProfilePhotoGoogle } from \"../../_utils/oauth/updateProfilePhotoGoogle\";\nimport { getGoogleAppKeys } from \"../lib/getGoogleAppKeys\";\n\nasync function getHandler(req: NextApiRequest, res: NextApiResponse) {\n  const { code } = req.query;\n  const state = decodeOAuthState(req);\n\n  if (typeof code !== \"string\") {\n    if (state?.onErrorReturnTo || state?.returnTo) {\n      res.redirect(\n        getSafeRedirectUrl(state.onErrorReturnTo) ??\n          getSafeRedirectUrl(state?.returnTo) ??\n          `${WEBAPP_URL}/apps/installed`\n      );\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 getGoogleAppKeys();\n\n  const redirect_uri = `${WEBAPP_URL_FOR_OAUTH}/api/integrations/googlecalendar/callback`;\n\n  const oAuth2Client = new OAuth2Client(client_id, client_secret, redirect_uri);\n\n  if (code) {\n    const token = await oAuth2Client.getToken(code);\n    const key = token.tokens;\n    const grantedScopes = token.tokens.scope?.split(\" \") ?? [];\n    // Check if we have granted all required permissions\n    const hasMissingRequiredScopes = GOOGLE_CALENDAR_SCOPES.some((scope) => !grantedScopes.includes(scope));","sourceCodeStart":21,"sourceCodeEnd":57,"githubUrl":"https://github.com/calcom/cal.diy/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/app-store/googlecalendar/api/callback.ts#L21-L57","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nif (typeof code !== \"string\") {\n  if (state?.onErrorReturnTo || state?.returnTo) { res.redirect(...); return; }\n  throw new HttpError({ statusCode: 400, message: \"`code` must be a string\" });\n}\n// after - distinguish explicit Google denial from a malformed request\nif (typeof code !== \"string\") {\n  if (state?.onErrorReturnTo || state?.returnTo) { res.redirect(...); return; }\n  const denied = \"error\" in req.query;\n  throw new HttpError({\n    statusCode: 400,\n    message: denied ? \"Google authorization was denied\" : \"`code` must be a string\",\n  });\n}","handlingStrategy":"validation","validationCode":"// Before processing the callback, validate the query shape and detect explicit denial\nfunction readOAuthCode(query: NodeJS.Query): string | null {\n  const c = query.code;\n  if (typeof c === \"string\" && c.length > 0) return c;\n  return null; // missing or duplicated — caller should treat as denial/malformed\n}\n\nconst code = readOAuthCode(req.query as NodeJS.Query);\nif (!code && !(\"error\" in req.query)) {\n  // genuinely malformed request, not a user denial\n}","typeGuard":"function isStringCode(value: unknown): value is string {\n  return typeof value === \"string\" && value.length > 0;\n}","tryCatchPattern":null,"preventionTips":["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."],"tags":["app-store","googlecalendar","oauth","callback","http-400"],"backgroundTag":null,"analyzedSha":"176037d0afbe572f870a3c702985e7cd83fe6c0c","analyzedAt":"2026-08-12T19:12:41.464Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}