Hmbown/CodeWhale · error · anyhow::Error

invalid SSE JSON

Error message

invalid SSE JSON: {e}

What it means

This error is raised by `convert_anthropic_sse_data` in the Anthropic SSE client when a raw `data:` line from the stream cannot be parsed as JSON by the fallback legacy path. The function first tries a typed decode; on failure it attempts `serde_json::from_str`, and if even that fails, the line is not valid JSON at all, so the stream yields a hard error and terminates.

Solutions

  1. Log the raw `data:` line before parsing to inspect the exact malformed payload
  2. Check whether a proxy/gateway is truncating or rewriting SSE chunks and bypass or fix it
  3. Verify the base URL points at the real Anthropic Messages API
  4. Fix the malformed fixture if this comes from a test fixture

Example fix

// before: fixture with truncated JSON
{"type":"content_block_delta","delta":{"type":"text_delta","text":"hi"
// after: complete JSON object per SSE data line
{"type":"content_block_delta","delta":{"type":"text_delta","text":"hi"}}
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-validate a fixture line before feeding the SSE parser
fn is_valid_json_line(line: &str) -> bool {
    serde_json::from_str::<serde_json::Value>(line.trim()).is_ok()
}

Type guard

fn parse_sse_json(line: &str) -> Option<serde_json::Value> {
    serde_json::from_str(line.trim()).ok()
}

Try / catch

match result {
    Ok(event) => handle(event),
    Err(e) if e.to_string().contains("invalid SSE JSON") => {
        log_raw_line(&raw); // capture payload for diagnosis
        fail_stream(e);
    }
    Err(e) => fail_stream(e),
}

Prevention

When it happens

Trigger: An Anthropic SSE `data:` frame contains malformed JSON — e.g. truncated frames from a proxy buffering bug, a mock/test fixture with bad JSON, or a non-Anthropic endpoint behind an Anthropic-compatible base URL emitting non-JSON payloads.

Common situations: Corporate proxies or gateways that mangle chunked responses; pointing the client at an OpenAI-style or self-hosted endpoint that emits malformed lines; hand-written SSE fixtures in tests with typos in the JSON.

Understand the failure class

Background: JSON parse error: "Unexpected token" / "not valid JSON" / "failed to parse" — what JSON parsers are really complaining about — this error's family across 45 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/e8c2aaa541265dc9. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/client/anthropic.rs:896

    }
    let usage_event = matches!(
        serde_json::from_str::<SseTagPeek>(trimmed).map(|peek| peek.r#type),
        Ok(Some("message_start" | "message_delta"))
    );
    if usage_event {
        return convert_anthropic_sse_usage_event(trimmed);
    }
    match serde_json::from_str::<StreamEvent>(trimmed) {
        // Local-only receipt: the legacy path ignored it (not a provider
        // type), so it stays ignored rather than decoding.
        Ok(StreamEvent::ToolProjectionWarning { .. }) => None,
        Ok(event) => Some(Ok(event)),
        Err(error) => {
            // Cold path, reached only when direct decode fails: invalid JSON
            // and unknown types keep their exact legacy outcomes.
            let value: Value = match serde_json::from_str(trimmed) {
                Ok(value) => value,
                Err(e) => return Some(Err(anyhow::anyhow!("invalid SSE JSON: {e}"))),
            };
            match value.get("type").and_then(Value::as_str) {
                // Tolerate unknown event types (e.g. future additions) silently.
                Some(known) if !is_known_sse_type(known) => None,
                _ => Some(Err(anyhow::anyhow!("unrecognized SSE event: {error}"))),
            }
        }
    }
}

/// Legacy `Value` path for `message_start`/`message_delta`: the usage
/// rewrite reads wire fields the normalized [`Usage`] cannot represent, so
/// these two events normalize before decoding, exactly as before.
fn convert_anthropic_sse_usage_event(trimmed: &str) -> Option<Result<StreamEvent>> {
    let mut value: Value = match serde_json::from_str(trimmed) {
        Ok(value) => value,
        Err(e) => return Some(Err(anyhow::anyhow!("invalid SSE JSON: {e}"))),
    };

View on GitHub (pinned to 73e0f67d83)