{"record":{"id":"d1517cd80d55e63b","repo":"Significant-Gravitas/AutoGPT","slug":"oauth2-callback-failed-to-exchange-code-for-tokens","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/external/v1/integrations.py","lineNumber":455,"sourceCode":"\n        credentials = await handler.exchange_code_for_tokens(\n            request.code, scopes, valid_state.code_verifier\n        )\n\n        # Handle Linear's space-separated scopes\n        if len(credentials.scopes) == 1 and \" \" in credentials.scopes[0]:\n            credentials.scopes = credentials.scopes[0].split(\" \")\n\n        # Check scope mismatch\n        if not set(scopes).issubset(set(credentials.scopes)):\n            logger.warning(\n                f\"Granted scopes {credentials.scopes} for provider {provider} \"\n                f\"do not include all requested scopes {scopes}\"\n            )\n\n    except Exception as e:\n        logger.error(f\"OAuth2 Code->Token exchange failed for provider {provider}: {e}\")\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    # Store credentials\n    await creds_manager.create(auth.user_id, credentials)\n\n    logger.info(f\"Successfully completed external OAuth for provider {provider}\")\n\n    return OAuthCompleteResponse(\n        credentials_id=credentials.id,\n        provider=credentials.provider,\n        type=credentials.type,\n        title=credentials.title,\n        scopes=credentials.scopes,\n        username=credentials.username,\n        state_metadata=valid_state.state_metadata,\n    )","sourceCodeStart":437,"sourceCodeEnd":473,"githubUrl":"https://github.com/Significant-Gravitas/AutoGPT/blob/9c8bb5550f446ba5d3046b78896578742495b3cf/autogpt_platform/backend/backend/api/external/v1/integrations.py#L437-L473","documentation":"Raised (HTTP 400) by the external OAuth complete endpoint when `handler.exchange_code_for_tokens(...)` (or the subsequent default-scopes/scope-normalization block) raises any exception. The original exception is logged as 'OAuth2 Code->Token exchange failed for provider {provider}' and its string is surfaced in the detail, so the provider's error message (invalid_grant, redirect_uri mismatch, network failure) is embedded verbatim.","triggerScenarios":"POST `/integrations/{provider}/oauth/callback` with an expired or already-used authorization code, a redirect_uri that differs from the one used at authorize time, wrong client credentials, a provider outage/network error, or a provider response that fails parsing.","commonSituations":"Redeeming an auth code after the typical 5–10 minute provider TTL; retrying the callback after a first failure (codes are single-use); the platform's provider OAuth app redirect URI not covering the external callback; clock skew or revoked provider app.","solutions":["Read the detail message: 'invalid_grant' or 'code already redeemed' means restart the flow from authorize; 'redirect_uri mismatch' means align the callback URL used in both steps with the provider app config.","Complete the callback immediately after the user is redirected back; never retry with the same code.","Verify the provider OAuth app's allowed redirect URIs include the external callback origin.","Check backend logs (`OAuth2 Code->Token exchange failed for provider ...`) for the full provider response when the detail is opaque."],"exampleFix":"# before: retrying a consumed authorization code\nPOST /integrations/github/oauth/callback {\"state_token\": st, \"code\": \"reused_code\"}  # 400\n\n# after: get a fresh code by restarting the flow\nPOST /integrations/github/oauth/authorize -> user authorizes -> new code\nPOST /integrations/github/oauth/callback {\"state_token\": new_st, \"code\": \"fresh_code\"}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try:\n    creds = client.post(f\"/integrations/{provider}/oauth/callback\", json={\"state_token\": state, \"code\": code})\nexcept HTTPError as e:\n    detail = e.response.text\n    if \"invalid_grant\" in detail or \"code\" in detail.lower():\n        # auth codes are single-use & short-lived: restart the whole flow\n        state, login_url = client.post(f\"/integrations/{provider}/oauth/authorize\", json=payload).json().values()\n        raise RestartFlow(login_url)  # send user back to provider consent\n    if \"redirect_uri\" in detail:\n        raise ConfigError(\"align callback URL with provider app settings\") from e\n    raise","preventionTips":["Exchange the code for tokens as soon as the user lands on your callback (codes expire in minutes).","Never retry a failed token exchange with the same code — restart the flow.","Register the exact external callback URI in the provider's OAuth app settings."],"tags":["oauth","token-exchange","authorization-code","provider"],"backgroundTag":null,"analyzedSha":"9c8bb5550f446ba5d3046b78896578742495b3cf","analyzedAt":"2026-08-14T17:17:21.957Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}