googleworkspace/cli · error · GwsError

Failed to parse message: {e}

Error message

Failed to parse message: {e}

What it means

After a successful HTTP 2xx from `GET /gmail/v1/users/me/messages/{id}?format=full`, `resp.json::<Value>()` failed — the body was not valid JSON. This means Gmail (or an intercepting proxy) returned a 200 with a non-JSON payload, e.g. an HTML error page, an empty body, or a truncated response.

Source

Thrown at crates/google-workspace-cli/src/helpers/gmail/mod.rs:402

    .map_err(|e| GwsError::Other(anyhow::anyhow!("Failed to fetch message: {e}")))?;

    if !resp.status().is_success() {
        let status = resp.status().as_u16();
        let body = resp
            .text()
            .await
            .unwrap_or_else(|_| "(error body unreadable)".to_string());
        return Err(build_api_error(
            status,
            &body,
            &format!("Failed to fetch message {message_id}"),
        ));
    }

    let msg: Value = resp
        .json()
        .await
        .map_err(|e| GwsError::Other(anyhow::anyhow!("Failed to parse message: {e}")))?;

    parse_original_message(&msg)
}

/// Build a `GwsError::Api` from an HTTP error response body, parsing the
/// Google JSON error format when possible. Modeled after the executor's
/// `handle_error_response`, extracting message, reason, and enable URL.
pub(super) fn build_api_error(status: u16, body: &str, context: &str) -> GwsError {
    let err_json: Option<Value> = serde_json::from_str(body).ok();
    let err_obj = err_json.as_ref().and_then(|v| v.get("error"));
    let message = err_obj
        .and_then(|e| e.get("message"))
        .and_then(|m| m.as_str())
        .unwrap_or(body)
        .to_string();
    let reason = err_obj
        .and_then(|e| e.get("errors"))
        .and_then(|e| e.as_array())

View on GitHub (pinned to a3768d0e82)

Solutions

  1. Retry the command — transient truncation resolves itself.
  2. Check whether a proxy/captive portal intercepts googleapis.com and bypass it.
  3. Reproduce the raw body: `curl -H "Authorization: Bearer $TOKEN" 'https://gmail.googleapis.com/gmail/v1/users/me/messages/<ID>?format=full' | head -c 200` to see if it is HTML.
  4. Verify the OAuth token is a real Gmail token, not one from a mock/stub server.

Example fix

// before: json() alone loses the body that failed to parse
let msg: Value = resp.json().await.map_err(|e| GwsError::Other(anyhow::anyhow!("Failed to parse message: {e}")))?;

// after: capture the raw body to surface what was actually returned
let text = resp.text().await.map_err(|e| GwsError::Other(anyhow::anyhow!("Failed to read message body: {e}")))?;
let msg: Value = serde_json::from_str(&text).map_err(|e| {
    GwsError::Other(anyhow::anyhow!("Failed to parse message: {e} (body starts: {})", &text[..text.len().min(120)]))
})?;
Defensive patterns

Strategy: validation

Validate before calling

// Assert JSON content-type before parsing, catching interceptor HTML early
let ct = resp.headers().get(reqwest::header::CONTENT_TYPE).and_then(|v| v.to_str().ok()).unwrap_or("");
if !ct.starts_with("application/json") {
    return Err(anyhow::anyhow!("expected JSON from Gmail, got '{ct}' — proxy interception?"));
}

Type guard

fn looks_like_gmail_message(v: &serde_json::Value) -> bool {
    v.get("id").and_then(|x| x.as_str()).is_some() && v.get("payload").map(|p| p.is_object()).unwrap_or(false)
}

Try / catch

// Parse via text() first so failures can include a body snippet:
let text = resp.text().await?;
match serde_json::from_str::<Value>(&text) {
    Ok(v) if looks_like_gmail_message(&v) => v,
    Ok(_) => return Err(anyhow::anyhow!("200 response missing id/payload fields")),
    Err(e) => return Err(anyhow::anyhow!("invalid JSON from Gmail: {e}; body[0..120]={}", &text[..text.len().min(120)])),
}

Prevention

When it happens

Trigger: Captive portal or proxy returning 200 with an HTML login page; response truncated by a flaky connection so the body is cut mid-JSON; a gateway rewriting responses; extremely rare API glitch serving an error page with 200.

Common situations: Corporate TLS-inspection appliances, hotel wifi, or a mid-response connection reset that reqwest still surfaces as a complete body read; scripts parsing via the CLI in sandboxed CI with weird egress.

Understand the failure class

Related errors


AI-assisted analysis of googleworkspace/cli@a3768d0e82 (2026-08-16). Data as JSON: /api/errors/a7c28b13cb529760. Report an issue: GitHub.