{"record":{"id":"633176f9b0d4063b","repo":"zed-industries/zed","slug":"error-deserializing-latest-release-err","errorCode":null,"errorMessage":"error deserializing latest release: {err:?}","messagePattern":"error deserializing latest release: (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/http_client/src/github.rs","lineNumber":74,"sourceCode":"\n    if response.status().is_client_error() {\n        let text = String::from_utf8_lossy(body.as_slice());\n        bail!(\n            \"status error {}, response: {text:?}\",\n            response.status().as_u16()\n        );\n    }\n\n    let releases = match serde_json::from_slice::<Vec<GithubRelease>>(body.as_slice()) {\n        Ok(releases) => releases,\n\n        Err(err) => {\n            log::error!(\"Error deserializing: {err:?}\");\n            log::error!(\n                \"GitHub API response text: {:?}\",\n                String::from_utf8_lossy(body.as_slice())\n            );\n            anyhow::bail!(\"error deserializing latest release: {err:?}\");\n        }\n    };\n\n    let mut release = releases\n        .into_iter()\n        .filter(|release| !require_assets || !release.assets.is_empty())\n        .find(|release| release.pre_release == pre_release)\n        .context(\"finding a prerelease\")?;\n    release.assets.iter_mut().for_each(|asset| {\n        if let Some(digest) = &mut asset.digest\n            && let Some(stripped) = digest.strip_prefix(\"sha256:\")\n        {\n            *digest = stripped.to_owned();\n        }\n    });\n    Ok(release)\n}\n","sourceCodeStart":56,"sourceCodeEnd":92,"githubUrl":"https://github.com/zed-industries/zed/blob/f4178619acd0d47ea1f76a2025c42962c6d6638c/crates/http_client/src/github.rs#L56-L92","documentation":"After the 4xx check passes, the release lister tries serde_json::from_slice::<Vec<GithubRelease>> on the body and bails when deserialization fails (it also logs the raw response text at error level). The body is valid HTTP 2xx/5xx content but not the expected array-of-releases shape: a 5xx HTML error page (server errors are NOT excluded by is_client_error), a proxy/interceptor payload, or a changed GitHub schema all trigger it.","triggerScenarios":"GitHub returning 5xx (the code only bails on client errors, so HTML error pages reach the parser), a corporate proxy rewriting responses, an API response where tag_name/assets/tarball_url/zipball_url fields are missing or renamed.","commonSituations":"Transient GitHub incidents; behind MITM proxies that inject HTML; upstream API contract changes after GitHub deprecates fields; pagination payloads accidentally routed to this function.","solutions":["Look at the logged 'GitHub API response text:' line to see what body was actually parsed","Treat 5xx as an error before parsing (check !status.is_success() instead of only is_client_error) and retry with backoff","Update the GithubRelease struct if GitHub changed the schema","Bypass the proxy or add exceptions for api.github.com"],"exampleFix":"// before\nif response.status().is_client_error() { /* bail */ }\nlet releases = serde_json::from_slice::<Vec<GithubRelease>>(&body)?;\n\n// after\nlet status = response.status();\nif !status.is_success() {\n    let text = String::from_utf8_lossy(&body);\n    anyhow::bail!(\"status error {}, response: {text:?}\", status.as_u16());\n}\nlet releases = serde_json::from_slice::<Vec<GithubRelease>>(&body)?;","handlingStrategy":"try-catch","validationCode":"// reject non-2xx (including 5xx HTML pages) before parsing\nlet status = response.status();\nif !status.is_success() { anyhow::bail!(\"status {}\", status.as_u16()); }","typeGuard":null,"tryCatchPattern":"match serde_json::from_slice::<Vec<GithubRelease>>(&body) {\n    Ok(releases) => Ok(releases),\n    Err(err) => {\n        log::error!(\"response text: {:?}\", String::from_utf8_lossy(&body));\n        retry_with_backoff().await // 5xx pages are transient\n    }\n}","preventionTips":["Check is_success(), not just is_client_error(), before deserializing external API bodies","Log the raw body next to the serde error (this code already does) so schema drift is diagnosable","Pin API versions / keep the DTO struct in sync with the documented response shape"],"tags":["github","serde","json","deserialization","api"],"backgroundTag":"api-response-schema-changed","analyzedSha":"f4178619acd0d47ea1f76a2025c42962c6d6638c","analyzedAt":"2026-08-20T19:29:52.058Z","contentChangedAt":"2026-08-20T19:29:52.058Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}