xtekky/gpt4free · error · RuntimeError

No access_token in refresh response.

Error message

No access_token in refresh response.

What it means

The refresh endpoint answered HTTP 200 but the JSON body had no access_token field. Google's token endpoint always returns access_token on success, so this indicates a malformed or intercepted response (captive portal, HTML error page parsed as JSON, or an API surface change), not a normal OAuth condition.

Solutions

  1. Log the full response body to see what actually came back (HTML challenge page vs JSON)
  2. Retry once after clearing the token cache; if it persists, re-login to get a new refresh token
  3. Check proxy/TLS interception: curl https://oauth2.googleapis.com/token should return JSON
  4. Update g4f in case the endpoint or response handling changed upstream
Defensive patterns

Strategy: retry

Try / catch

for attempt in range(2):
    try:
        return await auth_manager.get_access_token()
    except RuntimeError as e:
        if "No access_token in refresh response" in str(e) and attempt == 0:
            await auth_manager.clear_token_cache()
            continue
        raise

Prevention

When it happens

Trigger: session.post to OAUTH_REFRESH_URL returns 200 with a body missing the access_token key; resp.json() succeeds but resp_data.get('access_token') is None or empty.

Common situations: Transparent proxy or captive portal returning a 200 HTML page; a rare Google-side transient; response schema change after an endpoint update.

Related errors


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

Appendix: source

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

            "client_id": self.OAUTH_CLIENT_ID,
            "client_secret": self.OAUTH_CLIENT_SECRET,
            "refresh_token": refresh_token,
            "grant_type": "refresh_token",
        }

        async with aiohttp.ClientSession() as session:
            async with session.post(
                self.OAUTH_REFRESH_URL, data=data, headers=headers
            ) as resp:
                if resp.status != 200:
                    text = await resp.text()
                    raise RuntimeError(f"Token refresh failed: {text}")
                resp_data = await resp.json()
                access_token = resp_data.get("access_token")
                expires_in = resp_data.get("expires_in", 3600)  # seconds

                if not access_token:
                    raise RuntimeError("No access_token in refresh response.")

                self._access_token = access_token
                self._expiry = time.time() + expires_in

                expiry_date_ms = int(self._expiry * 1000)  # milliseconds

                await self._cache_token(access_token, expiry_date_ms)

    async def _cache_token(self, access_token: str, expiry_date: int) -> None:
        # Cache token in KV store or fallback to memory cache
        token_data = {
            "access_token": access_token,
            "expiry_date": expiry_date,
            "cached_at": int(time.time() * 1000),  # ms
        }
        self._token_cache[self.KV_TOKEN_KEY] = token_data

    async def _get_cached_token(self) -> Optional[Dict[str, Any]]:

View on GitHub (pinned to 973504e177)