xtekky/gpt4free · error · RuntimeError

Token exchange failed: {error_text}

Error message

Token exchange failed: {error_text}

What it means

The POST to https://oauth2.googleapis.com/token exchanging the authorization code returned a non-OK status; the error body is embedded. Common bodies: invalid_grant (code already used or expired — codes are single-use and live roughly 10 minutes), mismatching code_verifier, or redirect_uri mismatch with GEMINICLI_REDIRECT_URI.

Source

Thrown at g4f/Provider/needs_auth/GeminiCLI.py:1144

        async with aiohttp.ClientSession() as session:
            token_data = {
                "client_id": AuthManager.OAUTH_CLIENT_ID,
                "client_secret": AuthManager.OAUTH_CLIENT_SECRET,
                "code": code,
                "grant_type": "authorization_code",
                "redirect_uri": GEMINICLI_REDIRECT_URI,
                "code_verifier": verifier,
            }

            async with session.post(
                "https://oauth2.googleapis.com/token",
                data=token_data,
                headers={"Content-Type": "application/x-www-form-urlencoded"},
            ) as resp:
                if not resp.ok:
                    error_text = await resp.text()
                    raise RuntimeError(f"Token exchange failed: {error_text}")

                token_response = await resp.json()

            access_token = token_response.get("access_token")
            refresh_token = token_response.get("refresh_token")
            expires_in = token_response.get("expires_in", 3600)

            if not access_token or not refresh_token:
                raise RuntimeError("Missing tokens in response")

            # Get user info
            email = None
            async with session.get(
                "https://www.googleapis.com/oauth2/v1/userinfo?alt=json",
                headers={"Authorization": f"Bearer {access_token}"},
            ) as resp:
                if resp.ok:
                    user_info = await resp.json()

View on GitHub (pinned to 973504e177)

Solutions

  1. Start a fresh login attempt to get a new authorization code — codes are single-use
  2. Ensure the exact same redirect URI is used in both steps (use the built-in callback server, not a hand-edited URL)
  3. Check the embedded body: invalid_grant means stale code; mismatch means PKCE or redirect mismatch
  4. Complete the exchange promptly (codes expire in minutes)
Defensive patterns

Strategy: try-catch

Try / catch

try:
    tokens = await GeminiCLI.exchange_code_for_tokens(code, state)
except RuntimeError as e:
    msg = str(e)
    if "Token exchange failed" in msg and "invalid_grant" in msg:
        # code consumed or expired: only a fresh code helps
        raise AuthRestart("restart login to get a new authorization code")
    raise

Prevention

When it happens

Trigger: Exchanging an authorization code that was already consumed once, pasted a second time, or older than its short validity; PKCE verifier from the state does not match the challenge sent at authorization; redirect_uri differs between the auth request and token request.

Common situations: Browser pre-fetch or middleware consumes the code via the localhost callback before the manual path uses it; retrying after a failure with the same code; clock drift; copy/paste truncation of the code.

Related errors


AI-assisted analysis of xtekky/gpt4free@973504e177 (2026-08-14). Data as JSON: /api/errors/afb020e89a0c7a6c. Report an issue: GitHub.