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

  1. Update the app/but-update crate to the release matching the current endpoint schema
  2. Fetch the URL manually and inspect the JSON to see which fields differ
  3. 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

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

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20). Data as JSON: /api/errors/a9d1e14b29ec145b. Report an issue: GitHub.