gitbutlerapp/gitbutler · warning · anyhow::Error
Failed to parse response: {e}
Error message
Failed to parse response: {e} What it means
The server returned a 2xx response but its JSON body did not deserialize into CheckUpdateStatus — fields missing, wrong types, or unexpected shape. The endpoint's contract changed relative to this client's expectations, or a proxy served an HTML error page with 200.
Source
Thrown at crates/but-update/src/check.rs:132
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| anyhow::anyhow!("Failed to create runtime: {e}"))?;
runtime.block_on(async {
let response = client
.post(url)
.json(&request_body)
.send()
.await
.map_err(|e| anyhow::anyhow!("Request failed: {e}"))?
.error_for_status()
.map_err(|e| anyhow::anyhow!("Server returned error: {e}"))?;
let update_info = response
.json::<CheckUpdateStatus>()
.await
.map_err(|e| anyhow::anyhow!("Failed to parse response: {e}"))?;
Ok(update_info)
})
})
.join()
.map_err(|_| anyhow::anyhow!("Update check thread panicked"))?;
// Save to cache (convert to but_db types)
if let Ok(status) = &result {
let now = chrono::Utc::now();
// Get existing cache to preserve suppression
let existing = trans.update_check().get();
let (suppressed_at, suppress_duration_hours) = existing
.and_then(|cached| cached.suppressed_at.zip(cached.suppress_duration_hours))
.and_then(|(suppressed_at, duration_hours)| {
let suppress_until = suppressed_at + chrono::Duration::hours(duration_hours as i64);
if now > suppress_until {View on GitHub (pinned to caf1f223d3)
Solutions
- Update the app/but-update crate to the release matching the current endpoint schema
- Fetch the URL manually and inspect the JSON to see which fields differ
- If behind a proxy, bypass it for app.gitbutler.com and retry
Defensive patterns
Strategy: fallback
Try / catch
match check_status(...) {
Err(e) if e.to_string().contains("Failed to parse response") => {
// schema drift or proxy page: keep the cached CheckUpdateStatus, skip this cycle
Ok(cache.read_update_status())
}
r => r,
} Prevention
- Pin app builds and update-service deploys together; deserialize leniently (serde defaults/Option) where the contract is evolving
- Fall back to the DB-cached update info when parsing fails
- Alert on parse failures in telemetry — they indicate endpoint regressions
When it happens
Trigger: Update server deploys a schema change (renamed/removed fields like version or download URL); an intercepting proxy returns a 200 HTML captive-portal page; truncated body.
Common situations: Version skew between an old app build and a newer update API; middleboxes rewriting responses.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- Failed to create runtime: {e}
- Request failed: {e}
- Server returned error: {e}
- Update check thread panicked
- BUG: Sensitive data cannot be serialized - it needs to be ex
AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20).
Data as JSON: /api/errors/a9d1e14b29ec145b.
Report an issue: GitHub.