Hmbown/CodeWhale · error
returned HTTP with content type ; expected JSON
Error message
{operation} returned HTTP {status} with content type {content_type}; expected JSON{limit} What it means
`parse_oauth_json` validates the raw HTTP response from an OAuth discovery/device-grant/polling request. When the body cannot be parsed as JSON, it fails with the HTTP status, the response content type, and a note on whether the body exceeded the 64 KiB diagnostic limit — so the developer can see whether the server returned an HTML error page, an oversized body, or some other non-JSON payload.
Solutions
- Check the reported status/content-type: an HTML 4xx/5xx means the endpoint or network path is wrong — fix the issuer/endpoint URL or proxy
- Open the OAuth endpoint in a browser/curl and confirm it returns application/json
- If the body exceeded 64 KiB, fetch the endpoint directly and inspect what oversized payload is being served
Example fix
// before: issuer points at an HTML landing page "issuer": "https://example.com" // after: use the real authorization server root "issuer": "https://auth.example.com"
Defensive patterns
Strategy: try-catch
Validate before calling
let resp = reqwest::get(oauth_endpoint).await?;
let ct = resp.headers().get(CONTENT_TYPE).and_then(|v| v.to_str().ok()).unwrap_or("");
let body = resp.text().await?;
if !ct.starts_with("application/json") {
anyhow::bail!("endpoint did not return JSON (content-type: {ct}) — check issuer/proxy");
}
if body.len() > 64 * 1024 { anyhow::bail!("response exceeds 64 KiB diagnostic limit"); }
serde_json::from_str::<serde_json::Value>(&body)?; Try / catch
match discover_oauth_endpoints(issuer) {
Ok(eps) => eps,
Err(e) if e.to_string().contains("expected JSON") => {
// message embeds status + content type; check for HTML/proxy pages
inspect_endpoint_rawly(issuer)?;
Err(e)
}
Err(e) => Err(e),
} Prevention
- Verify the issuer/endpoint URL returns application/json before wiring it up (curl -i)
- Check for corporate proxies/captive portals that inject HTML into responses
- Keep OAuth responses under 64 KiB; investigate any oversized payload at the source
- Monitor the status and content-type embedded in the error message — they usually identify the culprit
When it happens
Trigger: `discover_oauth_endpoints`, `request_device_grant`, or `poll_device_grant` receives an HTTP response whose body fails `serde_json::from_slice` — e.g. an HTML error page from a proxy, a 502 gateway page, a text/plain message, or a body over 64 KiB (flagged as truncated).
Common situations: Corporate proxy/captive portal intercepting the OAuth request and returning HTML; wrong issuer URL pointing at a non-OAuth endpoint; server down with an HTML maintenance page; a misconfigured content type on a JSON endpoint that actually returns form-encoded data.
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
- Missing release response
- OAuth returned HTTP that was not token JSON
- OIDC discovery failed with HTTP
- OAuth callback must be GET
- OAuth device-code request failed
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/4b365eb3e76abf1c.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/oauth.rs:533
joined
}
};
let mut reader = response.take(OAUTH_RESPONSE_BODY_LIMIT + 1);
let mut body = Vec::new();
reader
.read_to_end(&mut body)
.with_context(|| format!("reading {operation} response"))?;
let truncated = body.len() as u64 > OAUTH_RESPONSE_BODY_LIMIT;
if truncated {
body.truncate(OAUTH_RESPONSE_BODY_LIMIT as usize);
}
let parsed = serde_json::from_slice(&body).map_err(|_| {
let limit = if truncated {
" (body exceeded the 64 KiB diagnostic limit)"
} else {
""
};
anyhow::anyhow!(
"{operation} returned HTTP {status} with content type {content_type}; expected JSON{limit}"
)
})?;
Ok((status, parsed))
}
fn bounded_oauth_error_text(raw: &str) -> String {
let mut output = String::with_capacity(raw.len().min(OAUTH_ERROR_DETAIL_LIMIT));
let mut previous_was_space = false;
let mut written = 0;
for character in raw.chars() {
let character = if character.is_whitespace() {
' '
} else if character.is_control() {
continue;
} else {
character
};View on GitHub (pinned to 73e0f67d83)