Hmbown/CodeWhale · error

Responses JSON body

Error message

Responses JSON body

What it means

Test-side `.expect("Responses JSON body")` panic from `serde_json::from_slice`. The captured request body is not valid JSON, so the test cannot inspect the Responses payload (`model`, `input`, absence of `messages`). This means the client sent a non-JSON body, a truncated body, or an SSE/other content type to the Responses endpoint.

Solutions

  1. Dump `&requests[0].body` bytes to see what was actually sent before parsing.
  2. Confirm the Responses wire encoder serializes the request envelope with `serde_json::to_vec`/`to_string`.
  3. Check the request `Content-Type` is `application/json` and no compression middleware altered the body.
  4. Fix the encoder in crates/tui/src/client.rs request building, not the test assertion.

Example fix

// before
let body: Value = serde_json::from_slice(&requests[0].body).expect("Responses JSON body");
// after (diagnosis)
let body: Value = serde_json::from_slice(&requests[0].body)
    .unwrap_or_else(|e| panic!("Responses JSON body: {e}; raw={:?}", String::from_utf8_lossy(&requests[0].body)));
Defensive patterns

Strategy: validation

Validate before calling

// Rust (pre-validate the captured body)
let raw = std::str::from_utf8(&requests[0].body).expect("body is UTF-8");
assert!(raw.trim_start().starts_with('{'), "Responses request body must be a JSON object: {raw}");

Type guard

fn json_object(body: &[u8]) -> Option<serde_json::Value> {
    serde_json::from_slice(body).ok().filter(|v| v.is_object())
}

Try / catch

let body: Value = serde_json::from_slice(&requests[0].body)
    .unwrap_or_else(|e| panic!("Responses JSON body: {e}; raw={}", String::from_utf8_lossy(&requests[0].body)));

Prevention

When it happens

Trigger: Running `opencode_zen_responses_request_uses_responses_route_without_oauth_headers` when `create_message_stream` serializes the Responses request as something other than JSON, the body got compressed/chunked unexpectedly, or the mock captured a body from a different request shape.

Common situations: Changing request serialization to a form-encoded or binary format by mistake; sending the streaming body instead of the JSON request envelope; accidental double-encoding or BOM prefix.

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/390b79eb6384e0aa. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/client.rs:8140

            )
            .expect(1)
            .mount(&server)
            .await;

        let client = opencode_zen_client(&server, "gpt-5.5");
        assert_eq!(client.wire_format, WireFormat::Responses);
        let mut stream = client
            .create_message_stream(minimal_zen_request("gpt-5.5"))
            .await
            .expect("Zen Responses request should start");
        while let Some(event) = stream.next().await {
            event.expect("Zen Responses stream event");
        }

        let requests = server.received_requests().await.expect("recorded request");
        assert_eq!(requests.len(), 1);
        assert_zen_bearer_without_codex_headers(&requests[0]);
        let body: Value = serde_json::from_slice(&requests[0].body).expect("Responses JSON body");
        assert_eq!(body.get("model").and_then(Value::as_str), Some("gpt-5.5"));
        assert!(body.get("input").is_some(), "Responses body: {body}");
        assert!(body.get("messages").is_none(), "Responses body: {body}");
    }

    #[tokio::test]
    async fn opencode_zen_messages_request_shape_uses_api_key_anthropic_route() {
        let server = MockServer::start().await;
        Mock::given(method("POST"))
            .and(path("/v1/messages"))
            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
                "id": "msg_zen",
                "type": "message",
                "role": "assistant",
                "content": [{"type": "text", "text": "ok"}],
                "model": "claude-sonnet-4-6",
                "stop_reason": "end_turn",
                "stop_sequence": null,

View on GitHub (pinned to 73e0f67d83)