Streamable HTTP MCP client parses Brotli-compressed JSON without decoding it
What issue are you seeing?
The Streamable HTTP MCP client can receive a JSON response with Content-Encoding: br, pass the compressed bytes directly to Serde, and fail with a misleading JSON error:
MCP client failed to start: MCP startup failed: Transport send error: Transport
[rmcp::transport::worker::WorkerTransport<rmcp::transport::streamable_http_client::StreamableHttpClientWorker<...>>]
error: Deserialize error: expected value at line 1 column 2
This occurs when initialize is small and uncompressed, but a larger tools/list response is Brotli-compressed by an ingress or reverse proxy.
Observed with Codex CLI 0.146.1 on macOS. Decompressing the same response externally produces valid JSON, while Codex passes the encoded body to the JSON parser.
What steps can reproduce the bug?
Run a minimal stateless MCP HTTP server that returns a Brotli-compressed tools/list response:
# pip install brotli
import brotli
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get("Content-Length", "0"))
request = json.loads(self.rfile.read(length))
method = request.get("method")
if method == "notifications/initialized":
self.send_response(202)
self.end_headers()
return
if method == "initialize":
result = {
"protocolVersion": request["params"]["protocolVersion"],
"capabilities": {},
"serverInfo": {"name": "brotli-repro", "version": "1.0.0"},
}
elif method == "tools/list":
result = {
"tools": [
{
"name": f"tool_{index}",
"description": "x" * 1000,
"inputSchema": {"type": "object", "properties": {}},
}
for index in range(100)
]
}
else:
result = {}
body = json.dumps(
{"jsonrpc": "2.0", "id": request.get("id"), "result": result}
).encode()
encoded = brotli.compress(body) if method == "tools/list" else body
self.send_response(200)
self.send_header("Content-Type", "application/json")
if method == "tools/list":
self.send_header("Content-Encoding", "br")
self.send_header("Content-Length", str(len(encoded)))
self.end_headers()
self.wfile.write(encoded)
HTTPServer(("127.0.0.1", 8765), Handler).serve_forever()
Configure Codex:
[mcp_servers.brotli_repro]
url = "http://127.0.0.1:8765/mcp"
Start Codex. MCP initialization reaches tools/list and then fails with the deserialize error.
What is the expected behavior?
Codex should do one of the following:
- Advertise only supported response codings, for example
Accept-Encoding: identity, when response decompression is unavailable; or - Enable Brotli support in the shared Reqwest client and decode the response before passing it to the JSON/SSE parser.
If an unsupported Content-Encoding is received, the error should identify the unsupported encoding instead of reporting malformed JSON.
Additional information
Root-cause analysis
In 0.146.1, StreamableHttpClientAdapter::post_message sets Accept and Content-Type, but does not set Accept-Encoding:
The shared HTTP client enables Reqwest features json, rustls-tls-native-roots, and stream, but not brotli:
https://github.com/openai/codex/blob/rust-v0.146.1/codex-rs/http-client/Cargo.toml#L7-L14
For an application/json response, the adapter collects the body stream and immediately calls serde_json::from_slice without inspecting Content-Encoding:
The same behavior is still present on current main at commit 7a0e974e08c798d1e8d59d407aeb6e24db1313af.
RFC 9110 section 12.5.3 says that when Accept-Encoding is absent, the user agent considers any content coding acceptable. Therefore a server or ingress can validly choose Brotli, even though this Codex client cannot decode it:
https://datatracker.ietf.org/doc/html/rfc9110#section-12.5.3
Reqwest's Brotli response decoding is gated by its optional brotli feature:
https://docs.rs/reqwest/latest/reqwest/struct.ClientBuilder.html#method.brotli
Workarounds
Client-side configuration:
[mcp_servers.example.http_headers]
Accept-Encoding = "identity"
Server-side: disable response compression for the MCP route.
5 Comments
test from automation
Thanks for the clear repro. This looks like a transport decode gap in streamable HTTP MCP.
Could you share one low-volume capture with headers (
Content-Encoding,Content-Type) plus a short raw-body excerpt, and confirm whether the endpoint is behind a proxy/CDN that could inject compression? Also includecodex --versionand MCP server/transport details so we can confirm whether to decodebronly when explicitly encoded JSON is received.Looking into this — root cause is that the Streamable HTTP MCP client never inspects
Content-Encodingonapplication/jsonresponses and hands the still-compressed bytes straight toserde_json, while the shared HTTP client (reqwest) is built without any decoding features. Planning a small, scoped fix:Accept-Encoding: identityon MCP HTTP requests so compliant servers won't compress responses.Content-Encodinganyway, fail with a clear "unsupported Content-Encoding" error instead of the misleading JSON parse error.Will open a PR shortly.
Thanks. Here is a sanitized low-volume capture from the failing
tools/listresponse.Codex version:
MCP server details:
json_response=True)POST /mcptools/list; the smallerinitializeresponse succeedsRelevant response headers:
I captured the wire body without automatic decompression. Its first bytes are:
After Brotli decompression, the body is valid JSON with the expected JSON-RPC shape:
As a control, sending:
returns the same valid JSON without
Content-Encoding: br, and Codex can parse it successfully.The production endpoint is behind a Kubernetes ingress/reverse proxy that can apply response compression. The FastMCP application itself does not configure Brotli compression, so the encoding appears to be added by the ingress layer. I have not included the endpoint or authorization headers here, but can provide further sanitized transport details if useful.
Given that the response explicitly includes both
Content-Type: application/jsonandContent-Encoding: br, decoding could be conditional on the declaredContent-Encodingbefore passing the body to the JSON parser. Alternatively, if the client does not support response decompression, sendingAccept-Encoding: identitywould prevent the server/proxy from selecting Brotli.Dug into this and got it fixed and verified locally.
Root cause: the streamable HTTP MCP client never sends an
Accept-Encodingheader and never checksContent-Encodingon the response before handing the body toserde_json. The shared HTTP client also doesn't have Brotli/gzip decoding enabled, so a compressed response from a proxy/ingress goes straight to the JSON parser as raw bytes — that's why you get the confusing "expected value at line 1 column 2" error instead of anything pointing at the real issue. Your capture above lines up exactly with this (Content-Type: application/json + Content-Encoding: br, and Accept-Encoding: identity as a workaround that sidesteps it entirely).What I changed: the client now sends
Accept-Encoding: identityon MCP requests, so a compliant server/proxy won't compress the response in the first place — this is the same behavior you confirmed manually. On top of that, if a server ignores that and compresses anyway, the client now catches it immediately and fails with a clear "unsupported Content-Encoding: br" error instead of silently trying to parse garbage as JSON.End result:
tools/list(and any other MCP response) behind a compressing proxy now either works correctly, or fails with an error that actually tells you what went wrong — no more mystery JSON parse errors. Wrote a test that mocks a Brotli-compressedtools/listresponse and confirms the client rejects it cleanly. Ran the full test suite plus clippy for the crate, everything passes, no regressions.Happy to open a PR for this if someone from the team wants to invite one — it's ready to go.