The no-reply failure of the Streamable HTTP client transport only reported the
request ID, even when the peer had responded with a different ID, an empty body,
or something that was not a JSON-RPC reply. Response bodies are only logged at
Trace level through LogTransportReceivedMessageSensitive, so the failure was not
diagnosable at default logging.
Track non-sensitive shape facts about the uncorrelated response (media type,
empty body, deserialization failures, the ID the peer actually replied with, a
network-terminated event stream) and append them to the exception message. No
payload is copied into the message, so the existing sensitive-logging boundary is
preserved and the original message prefix stays intact.
Add production-equivalent coverage for a chunked application/json reply: a
loopback socket answers HTTP 200 with Transfer-Encoding: chunked, no
Content-Length, and a correlated JSON-RPC error, driven through the transport's
normal ResponseHeadersRead path. The correlated error is delivered to the session
and drives the existing server/discover -> initialize fallback.
Related to modelcontextprotocol#1862
Summary
Addresses the two asks in #1862 for the Streamable HTTP client transport: production-equivalent coverage for a chunked application/json reply, and diagnostics on the no-reply failure.
1. Coverage for a chunked application/json reply (new StreamableHttpResponseCorrelationTests)
The existing coverage for this path uses an in-memory StringContent, which is already buffered and carries a Content-Length. The two new tests drive a raw loopback socket instead, answering with HTTP 200, Content-Type: application/json; charset=utf-8, Transfer-Encoding: chunked, no Content-Length, and the body split across 16-byte chunks — consumed through the transport's normal ResponseHeadersRead flow (the request body is written chunked as well, since JsonContent has no known length). They assert that a correlated JSON-RPC error is (a) delivered to the session and (b) drives the existing server/discover → initialize fallback to 2025-06-18.
Finding worth recording for the original report: both tests pass on current main with no transport change. I could not reproduce the loss described in the issue for a correlated error in a chunked application/json 200 response, including when the server/discover probe is answered with a -32600 error. So the framing itself does not appear to be the cause; the failure mode that remains reachable is a reply the transport cannot correlate.
2. The no-reply failure now reports what actually happened
McpException: Streamable HTTP POST response completed without a reply to request with ID: 1 was the only signal, even when the peer had clearly answered. Because response bodies are only logged at Trace level via LogTransportReceivedMessageSensitive, everything needed to diagnose the failure was invisible at default logging.
The transport now accumulates non-sensitive shape facts about the uncorrelated response and appends them to that message:
The reported cases are the four requested in the issue plus two cheap neighbours: empty JSON body; JSON-RPC deserialization failure; a message that is not a response/error for this request; a response or error whose ID differs from the request ID; a response or error with a null ID (JSON-RPC permits this when the peer could not determine the request ID); and a text/event-stream that ended with a network error. The response media type is always included.
Only message shape and IDs are reported. No message payload is copied into the exception message, so the existing sensitive-logging boundary is preserved, and the original ...without a reply to request with ID: X prefix is kept intact.
Verification
Related to #1862.