Significant-Gravitas/AutoGPT · error · HTTPException

OAuth2 callback failed to exchange code for tokens: {str(e)}

Error message

OAuth2 callback failed to exchange code for tokens: {str(e)}

What it means

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.

Source

Thrown at autogpt_platform/backend/backend/api/features/integrations/router.py:355

        # Linear returns scopes as a single string with spaces, so we need to split them
        # TODO: make a bypass of this part of the OAuth handler
        if len(credentials.scopes) == 1 and " " in credentials.scopes[0]:
            credentials.scopes = credentials.scopes[0].split(" ")

        # Check if the granted scopes are sufficient for the requested scopes
        if not set(scopes).issubset(set(credentials.scopes)):
            # For now, we'll just log the warning and continue
            logger.warning(
                f"Granted scopes {credentials.scopes} for provider {provider.value} "
                f"do not include all requested scopes {scopes}"
            )

    except Exception as e:
        logger.error(
            f"OAuth2 Code->Token exchange failed for provider {provider.value}: {e}"
        )
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"OAuth2 callback failed to exchange code for tokens: {str(e)}",
        )

    # TODO: Allow specifying `title` to set on `credentials`
    credentials = await _merge_or_create_credential(
        user_id, provider, credentials, valid_state.credential_id
    )

    logger.debug(
        f"Successfully processed OAuth callback for user {user_id} "
        f"and provider {provider.value}"
    )

    return to_meta_response(credentials)


# Bound the first-time sweep so a slow upstream (e.g. Ayrshare) can't hang

View on GitHub (pinned to 9c8bb5550f)

Solutions

  1. 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
  2. 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
  3. If invalid_client / unauthorized: verify the provider's client id/secret env vars are set in the backend environment and match the provider console
  4. 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
  5. Check the redirect URI registered with the provider exactly matches the callback URL the backend advertises

Example fix

// before: replaying an old callback body
await client.post(f'/integrations/{provider}/callback', json={'code': old_code, 'state_token': old_state})

// after: mint a fresh login, then callback immediately
login = await client.post(f'/integrations/{provider}/login', json={'scopes': [...]})
# ...user completes consent...
await client.post(f'/integrations/{provider}/callback', json={'code': fresh_code, 'state_token': login.state_token})
Defensive patterns

Strategy: retry

Try / catch

resp = await client.post(...)
if resp.status_code == 400 and 'exchange code for tokens' in resp.json()['detail']:
    # the suffix carries the provider error; treat expired/used codes as retryable
    login = await fresh_login(provider)  # restart flow
else:
    resp.raise_for_status()

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of Significant-Gravitas/AutoGPT@9c8bb5550f (2026-08-14). Data as JSON: /api/errors/a4d3f0790c2388a9. Report an issue: GitHub.