{"record":{"id":"a9d1e14b29ec145b","repo":"gitbutlerapp/gitbutler","slug":"failed-to-parse-response-e","errorCode":null,"errorMessage":"Failed to parse response: {e}","messagePattern":"Failed to parse response: (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"warning","filePath":"crates/but-update/src/check.rs","lineNumber":132,"sourceCode":"        let runtime = tokio::runtime::Builder::new_current_thread()\n            .enable_all()\n            .build()\n            .map_err(|e| anyhow::anyhow!(\"Failed to create runtime: {e}\"))?;\n\n        runtime.block_on(async {\n            let response = client\n                .post(url)\n                .json(&request_body)\n                .send()\n                .await\n                .map_err(|e| anyhow::anyhow!(\"Request failed: {e}\"))?\n                .error_for_status()\n                .map_err(|e| anyhow::anyhow!(\"Server returned error: {e}\"))?;\n\n            let update_info = response\n                .json::<CheckUpdateStatus>()\n                .await\n                .map_err(|e| anyhow::anyhow!(\"Failed to parse response: {e}\"))?;\n\n            Ok(update_info)\n        })\n    })\n    .join()\n    .map_err(|_| anyhow::anyhow!(\"Update check thread panicked\"))?;\n\n    // Save to cache (convert to but_db types)\n    if let Ok(status) = &result {\n        let now = chrono::Utc::now();\n\n        // Get existing cache to preserve suppression\n        let existing = trans.update_check().get();\n        let (suppressed_at, suppress_duration_hours) = existing\n            .and_then(|cached| cached.suppressed_at.zip(cached.suppress_duration_hours))\n            .and_then(|(suppressed_at, duration_hours)| {\n                let suppress_until = suppressed_at + chrono::Duration::hours(duration_hours as i64);\n                if now > suppress_until {","sourceCodeStart":114,"sourceCodeEnd":150,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-update/src/check.rs#L114-L150","documentation":"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.","triggerScenarios":"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.","commonSituations":"Version skew between an old app build and a newer update API; middleboxes rewriting responses.","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"],"exampleFix":null,"handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"match check_status(...) {\n    Err(e) if e.to_string().contains(\"Failed to parse response\") => {\n        // schema drift or proxy page: keep the cached CheckUpdateStatus, skip this cycle\n        Ok(cache.read_update_status())\n    }\n    r => r,\n}","preventionTips":["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"],"tags":["rust","json","serde","schema-drift","update-checker","but-update"],"backgroundTag":"response-schema-mismatch","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","schemaVersion":2},"datasetVersion":"2026-08-31T22:30:34.772Z"}