{"record":{"id":"a4d3f0790c2388a9","repo":"Significant-Gravitas/AutoGPT","slug":"oauth2-callback-failed-to-exchange-code-for-tokens-a4d3f0","errorCode":null,"errorMessage":"OAuth2 callback failed to exchange code for tokens: {str(e)}","messagePattern":"OAuth2 callback failed to exchange code for tokens: (.+?)","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"autogpt_platform/backend/backend/api/features/integrations/router.py","lineNumber":355,"sourceCode":"\n        # Linear returns scopes as a single string with spaces, so we need to split them\n        # TODO: make a bypass of this part of the OAuth handler\n        if len(credentials.scopes) == 1 and \" \" in credentials.scopes[0]:\n            credentials.scopes = credentials.scopes[0].split(\" \")\n\n        # Check if the granted scopes are sufficient for the requested scopes\n        if not set(scopes).issubset(set(credentials.scopes)):\n            # For now, we'll just log the warning and continue\n            logger.warning(\n                f\"Granted scopes {credentials.scopes} for provider {provider.value} \"\n                f\"do not include all requested scopes {scopes}\"\n            )\n\n    except Exception as e:\n        logger.error(\n            f\"OAuth2 Code->Token exchange failed for provider {provider.value}: {e}\"\n        )\n        raise HTTPException(\n            status_code=status.HTTP_400_BAD_REQUEST,\n            detail=f\"OAuth2 callback failed to exchange code for tokens: {str(e)}\",\n        )\n\n    # TODO: Allow specifying `title` to set on `credentials`\n    credentials = await _merge_or_create_credential(\n        user_id, provider, credentials, valid_state.credential_id\n    )\n\n    logger.debug(\n        f\"Successfully processed OAuth callback for user {user_id} \"\n        f\"and provider {provider.value}\"\n    )\n\n    return to_meta_response(credentials)\n\n\n# Bound the first-time sweep so a slow upstream (e.g. Ayrshare) can't hang","sourceCodeStart":337,"sourceCodeEnd":373,"githubUrl":"https://github.com/Significant-Gravitas/AutoGPT/blob/9c8bb5550f446ba5d3046b78896578742495b3cf/autogpt_platform/backend/backend/api/features/integrations/router.py#L337-L373","documentation":"The POST /integrations/{provider}/callback endpoint caught an exception while exchanging the OAuth authorization code for tokens (handler.exchange_code_for_tokens), and re-raised it as HTTP 400 with the underlying error text appended. Any failure inside the token-exchange block — network error, invalid/expired code, bad client config, missing code_verifier — funnels into this one message.","triggerScenarios":"POST /integrations/{provider}/callback with a code that is expired, already used, or mismatched to the client_id/redirect_uri; missing/misconfigured OAuth client env vars (e.g. GOOGLE_CLIENT_*_ID/SECRET); PKCE code_verifier lost between login and callback (stale state token); provider API unreachable (5xx/timeout).","commonSituations":"Env vars for the provider not set in backend/.env (works locally, fails in docker), clock skew or long delay between consent and callback so the code expires, re-using a callback URL after the state token was already consumed, redirect URI registered in the provider console not matching the callback URL the backend sends.","solutions":["Read the appended {str(e)} and the server log line 'OAuth2 Code->Token exchange failed' — the provider's own error (e.g. invalid_grant, invalid_client) names the real cause","If invalid_grant: restart the OAuth flow at GET /integrations/{provider}/login to get a fresh code and state token; do not replay the old callback","If invalid_client / unauthorized: verify the provider's client id/secret env vars are set in the backend environment and match the provider console","If the code_verifier mismatches: confirm the state_token passed is the one from the same login session (state stores the PKCE verifier) and that it hasn't expired","Check the redirect URI registered with the provider exactly matches the callback URL the backend advertises"],"exampleFix":"// before: replaying an old callback body\nawait client.post(f'/integrations/{provider}/callback', json={'code': old_code, 'state_token': old_state})\n\n// after: mint a fresh login, then callback immediately\nlogin = await client.post(f'/integrations/{provider}/login', json={'scopes': [...]})\n# ...user completes consent...\nawait client.post(f'/integrations/{provider}/callback', json={'code': fresh_code, 'state_token': login.state_token})","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"resp = await client.post(...)\nif resp.status_code == 400 and 'exchange code for tokens' in resp.json()['detail']:\n    # the suffix carries the provider error; treat expired/used codes as retryable\n    login = await fresh_login(provider)  # restart flow\nelse:\n    resp.raise_for_status()","preventionTips":["Never cache or replay authorization codes — they are single-use","Keep the state_token and code paired from the same login response","Set provider client id/secret env vars consistently across local, docker, and CI environments"],"tags":["oauth2","http","authentication","http-400"],"backgroundTag":null,"analyzedSha":"9c8bb5550f446ba5d3046b78896578742495b3cf","analyzedAt":"2026-08-14T17:17:21.957Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}