jlcodes99/cockpit-tools · error

网关 {} 响应解析失败: {} (url={})

Error message

网关 {} 响应解析失败: {} (url={})

What it means

post_gateway_json got a 2xx response but serde_json::from_str failed to parse the body as JSON, so it returns '网关 {op} 响应解析失败: {parse_err} (url={url})'. The gateway returned a non-JSON body (or invalid JSON) where a JSON payload was expected, e.g. an HTML error page or empty body.

Source

Thrown at src-tauri/src/modules/wakeup.rs:776

        crate::modules::logger::log_error(&format!(
            "[Wakeup] 网关 {} 返回错误: url={}, status={}, body_len={}, body={}",
            op_name,
            url,
            status,
            summarize_body_len(&text),
            truncate_log_text(&text, 512)
        ));
        return Err(format!(
            "网关 {} 返回错误: status={}, body_len={}",
            op_name,
            status,
            summarize_body_len(&text)
        ));
    }

    serde_json::from_str::<serde_json::Value>(&text).map_err(|e| {
        let message = format!("网关 {} 响应解析失败: {} (url={})", op_name, e, url);
        crate::modules::logger::log_error(&format!(
            "[Wakeup] {},body_len={}",
            message,
            summarize_body_len(&text)
        ));
        message
    })
}

async fn resolve_requested_model_for_official_ls(
    account_id: &str,
    model: &str,
    cancel_rx: Option<&watch::Receiver<bool>>,
) -> Result<serde_json::Value, String> {
    let trimmed = model.trim();
    if let Ok(num) = trimmed.parse::<i64>() {
        return Ok(json!({ "model": num }));
    }

View on GitHub (pinned to 1ed8b77992)

Solutions

  1. Log/inspect the truncated body (already truncated to 512 chars) to see what was actually returned
  2. Check for a proxy/VPN injecting HTML error pages into 200 responses
  3. Confirm the gateway endpoint version matches what this client expects
  4. Retry — transient truncation or gateway glitch may resolve
  5. Handle the empty-body case explicitly before parsing

Example fix

// before
serde_json::from_str::<serde_json::Value>(&text).map_err(|e| {
    let message = format!("网关 {} 响应解析失败: {} (url={})", op_name, e, url);
    ...
})
// after
if text.trim().is_empty() {
    return Err(format!("网关 {} 响应为空 (url={})", op_name, url));
}
serde_json::from_str::<serde_json::Value>(&text).map_err(|e| {
    let message = format!("网关 {} 响应解析失败: {} (url={})", op_name, e, url);
    ...
})
Defensive patterns

Strategy: validation

Validate before calling

// Guard the response before parsing
fn is_parseable_json(text: &str) -> bool {
    let t = text.trim();
    !t.is_empty() && (t.starts_with('{') || t.starts_with('[')) && serde_json::from_str::<serde_json::Value>(t).is_ok()
}

Type guard

fn as_json(text: &str) -> Option<serde_json::Value> {
    serde_json::from_str::<serde_json::Value>(text).ok()
}

Try / catch

match post_gateway_json(...).await {
    Err(e) if e.contains("响应解析失败") => {
        // inspect logged body snippet; likely proxy HTML or empty body — retry via alternate base
        try_next_base_url().await;
    }
    other => handle(other),
}

Prevention

When it happens

Trigger: trigger_wakeup_via_client_gateway_once → post_gateway_json receives text that isn't valid JSON — a proxy/HTML error page served with 200, an empty body, a truncated response, or a gateway returning plain text.

Common situations: Captive portal/corporate proxy injecting HTML; gateway returning empty 200 on internal error; compression/charset mismatch corrupting the body; gateway API version returning a different content type; response cut off by timeout mid-body.

Related errors


AI-assisted analysis of jlcodes99/cockpit-tools@1ed8b77992 (2026-09-05). Data as JSON: /api/errors/def341f15d071b51. Report an issue: GitHub.