{"record":{"id":"3a3bf57e8af36117","repo":"can1357/oh-my-pi","slug":"gitlab-oauth-token-exchange-failed-response-sta","errorCode":null,"errorMessage":"GitLab OAuth token exchange failed: ${response.status} ${await response.text()}","messagePattern":"GitLab OAuth token exchange failed: (.+?) (.+?)","errorType":"http","errorClass":"AIError.OAuthError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/registry/oauth/gitlab-duo.ts","lineNumber":164,"sourceCode":"\t\t\t\t\"Personal Access Token via GITLAB_TOKEN.\",\n\t\t};\n\t}\n\n\toverride async exchangeToken(code: string, _state: string, redirectUri: string): Promise<OAuthCredentials> {\n\t\tconst response = await this.#fetch(`${GITLAB_COM_URL}/oauth/token`, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n\t\t\tbody: new URLSearchParams({\n\t\t\t\tclient_id: this.#clientId,\n\t\t\t\tgrant_type: \"authorization_code\",\n\t\t\t\tcode,\n\t\t\t\tcode_verifier: this.#pkce.verifier,\n\t\t\t\tredirect_uri: redirectUri,\n\t\t\t}).toString(),\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tthrow new AIError.OAuthError(\n\t\t\t\t`GitLab OAuth token exchange failed: ${response.status} ${await response.text()}`,\n\t\t\t\t{\n\t\t\t\t\tkind: \"token-exchange\",\n\t\t\t\t\tprovider: \"gitlab-duo\",\n\t\t\t\t\tstatus: response.status,\n\t\t\t\t},\n\t\t\t);\n\t\t}\n\n\t\tclearGitLabDuoDirectAccessCache();\n\t\treturn mapTokenResponse(\n\t\t\t(await response.json()) as {\n\t\t\t\taccess_token?: string;\n\t\t\t\trefresh_token?: string;\n\t\t\t\texpires_in?: number;\n\t\t\t\tcreated_at?: number;\n\t\t\t},\n\t\t);","sourceCodeStart":146,"sourceCodeEnd":182,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/ai/src/registry/oauth/gitlab-duo.ts#L146-L182","documentation":"exchangeToken finishes the browser OAuth flow by POSTing the authorization code + PKCE verifier to https://gitlab.com/oauth/token. If GitLab answers non-2xx, this OAuthError is thrown with the status and response body in the message. It means the one-time authorization code could not be converted into tokens.","triggerScenarios":"POST /oauth/token with grant_type=authorization_code returns !ok: redirect_uri mismatch (GITLAB_REDIRECT_URI not exactly matching the GitLab app registration), expired or already-used authorization code, wrong PKCE code_verifier, invalid client_id, or revoked/invalidated code.","commonSituations":"The documented 'The redirect URI included is not valid' failure when the bundled client id's registered redirect list changed (issue #2424); retried logins reusing a consumed code; clock skew expiring the code; switching GITLAB_CLIENT_ID without updating GITLAB_REDIRECT_URI.","solutions":["Ensure GITLAB_REDIRECT_URI exactly matches a redirect URI registered on your GitLab OAuth application (strict string match).","Restart the login flow to obtain a fresh authorization code — codes are single-use and expire within ~10 minutes.","Register your own GitLab OAuth app and set both GITLAB_CLIENT_ID and GITLAB_REDIRECT_URI if the bundled credentials fail.","Bypass OAuth with GITLAB_TOKEN (Personal Access Token) if browser login is not viable.","Read the status/body in the message: 400 invalid_grant usually means code reuse/expiry; 401 invalid_client means wrong client id."],"exampleFix":"// before: client id overridden, redirect not updated\nGITLAB_CLIENT_ID=my-new-app-id\nGITLAB_REDIRECT_URI=http://localhost:9999/cb\n\n// after: pair must exactly match the GitLab app registration\nGITLAB_CLIENT_ID=my-new-app-id\nGITLAB_REDIRECT_URI=http://localhost:8080/callback","handlingStrategy":"retry","validationCode":"const redirectUri = process.env.GITLAB_REDIRECT_URI?.trim();\nconst clientId = process.env.GITLAB_CLIENT_ID?.trim();\nif (Boolean(clientId) !== Boolean(redirectUri)) {\n  console.warn(\"GITLAB_CLIENT_ID and GITLAB_REDIRECT_URI should be set together and match the GitLab app registration\");\n}","typeGuard":null,"tryCatchPattern":"let tokens: OAuthCredentials;\ntry {\n  tokens = await loginGitLabDuo(callbacks);\n} catch (err) {\n  if (err?.kind === \"token-exchange\" && err.status === 400) {\n    // code expired/consumed or redirect mismatch — restart the flow with a fresh code\n    tokens = await loginGitLabDuo(callbacks);\n  } else {\n    throw err;\n  }\n}","preventionTips":["Never retry with the same authorization code — codes are single-use and short-lived.","Keep GITLAB_REDIRECT_URI byte-identical to the URI registered on the GitLab application.","Set GITLAB_CLIENT_ID and GITLAB_REDIRECT_URI as a pair; changing one without the other breaks the exchange.","Complete the browser step promptly — codes expire in minutes."],"tags":["oauth","gitlab","token-exchange","pkce","network"],"backgroundTag":"oauth-code-exchange-failed","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}