janhq/jan · error · UpdateError

Failed to parse update response: {0}

Error message

Failed to parse update response: {0}

What it means

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.

Source

Thrown at src-tauri/src/core/updater/custom_updater.rs:34

use thiserror::Error;

/// Secret key for HMAC signature
/// - In CI: Set JAN_SIGNING_KEY environment variable at build time
/// - In local dev: Falls back to a test key
const SECRET_KEY: &str = match option_env!("JAN_SIGNING_KEY") {
    Some(key) => key,
    None => "local-dev-test-key-not-for-production",
};

/// Timeout for HTTP requests
const REQUEST_TIMEOUT_SECS: u64 = 30;

#[derive(Debug, Error)]
pub enum UpdateError {
    #[error("HTTP request failed: {0}")]
    RequestFailed(#[from] reqwest::Error),

    #[error("Failed to parse update response: {0}")]
    ParseError(String),

    #[error("All endpoints failed")]
    AllEndpointsFailed,

    #[error("Invalid response from server: {0}")]
    InvalidResponse(String),

    #[error("No endpoints configured")]
    NoEndpointsConfigured,
}

/// Update information returned by the update check endpoint
/// Compatible with Tauri's updater format
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateInfo {
    pub version: String,
    #[serde(default)]

View on GitHub (pinned to fad3f12a14)

Solutions

  1. Inspect the raw response body from the endpoint to see what was actually returned.
  2. Ensure the server returns at minimum `{"version": "x.y.z"}`.
  3. Check for CDN caching of an old or error response.
  4. Verify the endpoint URL has not changed and still serves the update manifest.

Example fix

# check what the endpoint actually returns
curl -s https://apps.jan.ai/update-check | jq .

# expected shape:
{"version": "1.2.3", "url": "...", "notes": "..."}
Defensive patterns

Strategy: validation

Validate before calling

// Validate the response shape before relying on it
function isValidUpdateInfo(data: unknown): data is { version: string } {
  return typeof data === 'object' && data !== null &&
    typeof (data as any).version === 'string' &&
    (data as any).version.length > 0;
}

// Or in Rust, use a lenient deserializer:
fn try_parse_update(body: &str) -> Result<UpdateInfo, UpdateError> {
    let v: serde_json::Value = serde_json::from_str(body)
        .map_err(|e| UpdateError::ParseError(e.to_string()))?;
    let version = v.get("version").and_then(|s| s.as_str())
        .ok_or_else(|| UpdateError::ParseError("missing 'version' field".into()))?;
    Ok(UpdateInfo { version: version.to_string(), /* ... */ })
}

Type guard

function isUpdateInfo(v: unknown): v is { version: string; url?: string } {
  return typeof v === 'object' && v !== null &&
    typeof (v as Record<string, unknown>).version === 'string';
}

Try / catch

Err(UpdateError::ParseError(msg)) => {
    log::warn!("Update endpoint returned unparseable response: {msg}");
    // Try the next fallback endpoint, or skip this update check
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Related errors


AI-assisted analysis of janhq/jan@fad3f12a14 (2026-08-12). Data as JSON: /api/errors/c90117b2fe17da63. Report an issue: GitHub.