Hmbown/CodeWhale · error
OAuth returned HTTP that was not token JSON
Error message
{name} OAuth {operation} returned HTTP {status} that was not token JSON What it means
This path parses an OAuth token-endpoint response body into `OAuthTokenMaterial` (name and operation are interpolated from the provider params for a clear message). If the body is not valid JSON, or lacks the token structure, the error states the provider name, operation, and HTTP status so the developer knows which request went wrong. It distinguishes 'not token JSON at all' from the structured token-error handling that follows.
Solutions
- Verify the token URL in the provider params points at the real token endpoint (usually .../token), not the authorize or documentation URL
- curl the token endpoint with a test request and confirm it returns application/json token data
- Check the HTTP status in the message: an HTML 200 from the wrong URL is a config error; 5xx is a server-side problem
Example fix
// before: token_url points at authorize page "token_url": "https://idp.example.com/authorize" // after "token_url": "https://idp.example.com/oauth/token"
Defensive patterns
Strategy: try-catch
Validate before calling
async fn token_endpoint_returns_json(url: &str) -> anyhow::Result<()> {
let body = reqwest::get(url).await?.text().await?;
if serde_json::from_str::<serde_json::Value>(&body).is_err() {
anyhow::bail!("token endpoint at {url} does not return JSON");
}
Ok("")
} Try / catch
match exchange_code_for_token(params, code) {
Ok(material) => material,
Err(e) if e.to_string().contains("not token JSON") => {
// provider name + operation + status are in the message:
// usually a wrong token_url or an HTML error page
log_raw_response_and_fix_token_url(params)?;
Err(e)
}
Err(e) => Err(e),
} Prevention
- Confirm token_url points at the token endpoint, not authorize/login pages
- Test the provider with curl before configuring it
- Be wary of development/stub servers that return non-JSON bodies
- Check provider docs for non-standard content types
When it happens
Trigger: A custom OAuth provider's token endpoint returns a non-JSON body (HTML login page, empty body, XML) for a token or refresh operation; a device-flow token poll receives an intermediate non-JSON response.
Common situations: Provider params misconfigured so the token URL hits a human-facing sign-in page; an OAuth server that responds to authorization_code exchange with a redirect/HTML; a captive portal replacing the response.
Understand the failure class
Background: "Invalid JSON response" and "Failed to parse response" errors: when an API answers 200 but the body isn't the JSON your library expected — this error's family across 28 libraries.
Related errors
- returned HTTP with content type ; expected JSON
- agy OAuth token JSON carries no access token member
- Codex credential file
- credential file is not valid credential JSON
- credential file must be a JSON object of entries
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/4860adcca7836e5e.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/oauth.rs:974
/// Pinned remote revoke URL; `None` when the provider revokes locally only.
pub(crate) fn remote_revoke_url(params: &OAuthProviderParams, issuer: &str) -> Option<String> {
params
.revoke_path
.map(|path| format!("{}/{}", issuer.trim_end_matches('/'), path))
}
/// Parse a form-post token response. Error bodies are never echoed: the
/// detail names the error code only, so a hostile issuer cannot smuggle
/// secret-bearing text back through diagnostics.
pub(crate) fn parse_oauth_form_response(
status: u16,
body: &str,
operation: &str,
params: &OAuthProviderParams,
) -> Result<OAuthTokenMaterial> {
let name = params.display_name;
let parsed: OAuthTokenMaterial = serde_json::from_str(body).map_err(|_| {
anyhow::anyhow!("{name} OAuth {operation} returned HTTP {status} that was not token JSON")
})?;
if !(200..300).contains(&status) || parsed.error.is_some() {
let err = parsed.error.as_deref().unwrap_or("token_error");
if matches!(
err,
"invalid_grant"
| "refresh_token_reused"
| "refresh_token_expired"
| "refresh_token_invalidated"
) || status == 401
{
bail!(
"{name} OAuth {operation} failed permanently ({err}). Sign in again with `{}`.",
params.relogin_hint
);
}
bail!("{name} OAuth {operation} failed ({err})");
}View on GitHub (pinned to 73e0f67d83)