{"record":{"id":"c90117b2fe17da63","repo":"janhq/jan","slug":"failed-to-parse-update-response-0","errorCode":null,"errorMessage":"Failed to parse update response: {0}","messagePattern":"Failed to parse update response: (.+?)","errorType":"exception","errorClass":"UpdateError","httpStatus":null,"severity":"error","filePath":"src-tauri/src/core/updater/custom_updater.rs","lineNumber":34,"sourceCode":"use thiserror::Error;\n\n/// Secret key for HMAC signature\n/// - In CI: Set JAN_SIGNING_KEY environment variable at build time\n/// - In local dev: Falls back to a test key\nconst SECRET_KEY: &str = match option_env!(\"JAN_SIGNING_KEY\") {\n    Some(key) => key,\n    None => \"local-dev-test-key-not-for-production\",\n};\n\n/// Timeout for HTTP requests\nconst REQUEST_TIMEOUT_SECS: u64 = 30;\n\n#[derive(Debug, Error)]\npub enum UpdateError {\n    #[error(\"HTTP request failed: {0}\")]\n    RequestFailed(#[from] reqwest::Error),\n\n    #[error(\"Failed to parse update response: {0}\")]\n    ParseError(String),\n\n    #[error(\"All endpoints failed\")]\n    AllEndpointsFailed,\n\n    #[error(\"Invalid response from server: {0}\")]\n    InvalidResponse(String),\n\n    #[error(\"No endpoints configured\")]\n    NoEndpointsConfigured,\n}\n\n/// Update information returned by the update check endpoint\n/// Compatible with Tauri's updater format\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct UpdateInfo {\n    pub version: String,\n    #[serde(default)]","sourceCodeStart":16,"sourceCodeEnd":52,"githubUrl":"https://github.com/janhq/jan/blob/fad3f12a147d138388a66f0d92a02b2675f65294/src-tauri/src/core/updater/custom_updater.rs#L16-L52","documentation":"The ParseError variant of UpdateError is returned when the update endpoint responded with HTTP 200 but the response body could not be deserialized into the UpdateInfo struct (version is required; notes, pub_date, platforms, url, signature are optional). This means the server returned valid JSON that is missing the required version field, or returned non-JSON content despite a 200 status.","triggerScenarios":"Server returns 200 with an empty body. Server returns JSON missing the required 'version' field. Server returns HTML (e.g. a CDN error page) with a 200 status. Server returns a JSON array instead of the expected object. Schema drift where the server changed its response format.","commonSituations":"CDN serving a cached error page with 200 status. Endpoint returning a different JSON schema after an API change. Misconfigured reverse proxy returning HTML error page with wrong status. Server returning `{}` (empty object) with no version.","solutions":["Inspect the raw response body from the endpoint to see what was actually returned.","Ensure the server returns at minimum `{\"version\": \"x.y.z\"}`.","Check for CDN caching of an old or error response.","Verify the endpoint URL has not changed and still serves the update manifest."],"exampleFix":"# check what the endpoint actually returns\ncurl -s https://apps.jan.ai/update-check | jq .\n\n# expected shape:\n{\"version\": \"1.2.3\", \"url\": \"...\", \"notes\": \"...\"}","handlingStrategy":"validation","validationCode":"// Validate the response shape before relying on it\nfunction isValidUpdateInfo(data: unknown): data is { version: string } {\n  return typeof data === 'object' && data !== null &&\n    typeof (data as any).version === 'string' &&\n    (data as any).version.length > 0;\n}\n\n// Or in Rust, use a lenient deserializer:\nfn try_parse_update(body: &str) -> Result<UpdateInfo, UpdateError> {\n    let v: serde_json::Value = serde_json::from_str(body)\n        .map_err(|e| UpdateError::ParseError(e.to_string()))?;\n    let version = v.get(\"version\").and_then(|s| s.as_str())\n        .ok_or_else(|| UpdateError::ParseError(\"missing 'version' field\".into()))?;\n    Ok(UpdateInfo { version: version.to_string(), /* ... */ })\n}","typeGuard":"function isUpdateInfo(v: unknown): v is { version: string; url?: string } {\n  return typeof v === 'object' && v !== null &&\n    typeof (v as Record<string, unknown>).version === 'string';\n}","tryCatchPattern":"Err(UpdateError::ParseError(msg)) => {\n    log::warn!(\"Update endpoint returned unparseable response: {msg}\");\n    // Try the next fallback endpoint, or skip this update check\n}","preventionTips":["Validate the response shape before deserializing — check for the 'version' field.","Log the raw response body when parsing fails so the server-side issue is visible.","Coordinate response schema with the update endpoint maintainer.","Use lenient deserialization with serde defaults for optional fields."],"tags":["updater","json","parse-error","response","schema"],"backgroundTag":null,"analyzedSha":"fad3f12a147d138388a66f0d92a02b2675f65294","analyzedAt":"2026-08-12T20:33:47.516Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}