{"record":{"id":"8b644e9e5d965ada","repo":"Hmbown/CodeWhale","slug":"mcp-response-body-exceeds-max-bytes-bytes-abor","errorCode":null,"errorMessage":"MCP response body exceeds {max_bytes} bytes — aborting","messagePattern":"MCP response body exceeds (.+?) bytes — aborting","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/mcp/streamable_http.rs","lineNumber":181,"sourceCode":"}\n\n/// Read a response body through the byte stream, failing as soon as it\n/// exceeds `max_bytes`. This bounds chunked and missing-Content-Length\n/// responses exactly like declared ones (the declared-length fast path in\n/// `send` only covers servers honest enough to announce their size).\n/// MCP bodies are JSON or SSE, so lossy UTF-8 matches `.text()` behavior.\npub(super) async fn read_body_capped(\n    response: reqwest::Response,\n    max_bytes: usize,\n) -> Result<String> {\n    use futures_util::StreamExt;\n\n    let mut stream = response.bytes_stream();\n    let mut buf: Vec<u8> = Vec::new();\n    while let Some(chunk) = stream.next().await {\n        let chunk = chunk.context(\"failed to read MCP response body\")?;\n        if buf.len().saturating_add(chunk.len()) > max_bytes {\n            anyhow::bail!(\"MCP response body exceeds {max_bytes} bytes — aborting\");\n        }\n        buf.extend_from_slice(&chunk);\n    }\n    Ok(String::from_utf8_lossy(&buf).into_owned())\n}\n\nfn is_streamable_http_incompatible_status(status: StatusCode) -> bool {\n    matches!(\n        status,\n        StatusCode::NOT_FOUND\n            | StatusCode::METHOD_NOT_ALLOWED\n            | StatusCode::NOT_ACCEPTABLE\n            | StatusCode::UNSUPPORTED_MEDIA_TYPE\n            | StatusCode::NOT_IMPLEMENTED\n    )\n}\n\nfn is_streamable_http_stale_session_status(status: StatusCode, body_excerpt: &str) -> bool {","sourceCodeStart":163,"sourceCodeEnd":199,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/crates/tui/src/mcp/streamable_http.rs#L163-L199","documentation":"The streamed body crossed max_bytes (MAX_MCP_RESPONSE_BYTES = 16 MiB) while read_body_capped() iterated the byte stream — the response was chunked or declared no Content-Length, so the upfront length guard could not catch it. Same OOM protection as the Content-Length guard, applied to the actual stream.","triggerScenarios":"A chunked or length-less MCP HTTP response whose accumulated body exceeds 16 MiB during the capped read.","commonSituations":"Servers streaming large tool outputs without a length; proxies re-chunking and stripping Content-Length; hostile servers lying about or omitting size.","solutions":["Reduce the response size server-side: pagination, filters, returning references","Check whether a gateway error page (huge HTML) replaced the real MCP response and fix the routing","Ensure origin servers emit bounded bodies even when a proxy re-chunks them"],"exampleFix":null,"handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"Identical to the declared-length variant: fall back to a narrower, paginated request; retrying the same call reproduces the same stream:\n```rust\nmatch fetch_resource(&id).await {\n    Err(e) if e.to_string().contains(\"MCP response body exceeds\") => {\n        fetch_resource_range(&id, offset, limit).await\n    }\n    other => other?,\n}\n```","preventionTips":["Cap tool output size server-side below the client's 16 MiB response ceiling","Watch for gateway error pages replacing real MCP responses — they can be the oversized body"],"tags":["mcp","http","limits","dos"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}