Skip to content

fix(transport): bound buffered Streamable HTTP responses - #1246

Merged
DaleSeo merged 3 commits into
modelcontextprotocol:mainfrom
GTanger:fix/streamable-http-response-limits
Nov 13, 2026
Merged

DaleSeo merged 3 commits into
modelcontextprotocol:mainfrom
GTanger:fix/streamable-http-response-limits

Conversation

@GTanger

@GTanger GTanger commented Sep 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Refs #1030. Addresses the Streamable HTTP client JSON/error response scope described in #1180 and redirected to #1030 by the maintainer. The stdio work in #1049 remains separate; this PR should not close the combined tracker on its own.

The built-in clients already bound individual SSE events, but buffer JSON and HTTP error bodies before parsing them. This change adds configurable pre-decode bounds without replacing the transports or adding dependencies.

Changes

  • Add StreamableHttpResponseLimits and transport config setters. Defaults: 16 MiB per complete JSON response, 64 KiB per HTTP diagnostic prefix, and the unchanged independent 16 MiB SSE-event limit.
  • Share bounded and prefix readers between reqwest and Unix socket backends. JSON is rejected with ResponseBodyTooLarge { max_size } if Content-Length exceeds its limit or a chunk would exceed it. Count bytes even when the length is missing or understated; do not drain oversized JSON to EOF.
  • For non-JSON HTTP error pages and unchallenged 401/403 responses (even when application/json), read_truncated_body retains at most the diagnostic limit and stops immediately when that prefix is full, without polling for another chunk or EOF. Large legacy 4xx pages can still reach the existing server/discover -> initialize fallback. The fallback eligibility rules are unchanged.
  • Other application/json error responses use the same complete-body JSON limit as successful responses. Parse the full bounded body before considering discovery fallback or truncating diagnostic text. Never parse a diagnostic prefix as a complete JSON-RPC message. Malformed/non-error JSON gets only a diagnostic prefix; oversized JSON still fails without fallback/retry.
  • Align the Unix socket backend with reqwest by forwarding complete HTTP JSON-RPC errors as StreamableHttpPostResponse::Json, before synthetic discovery fallback. This also fixes the previous backend discrepancy for these errors.
  • Forward limits through all six POST sending sites, including startup, discovery fallback, initialized notifications, ordinary/control requests, and session recovery. AuthClient preserves them across token refresh.
  • Keep the old POST entry points and provide a default implementation of the new trait method that delegates to the old SSE-limit override. Built-in backends apply the new defaults through old entry points too.

Compatibility and limits

Custom HTTP backends remain source-compatible, but must override post_message_with_response_limits to enforce JSON/diagnostic limits; the default cannot bound a body already decoded by custom code. The API docs explain this distinction and the JSON-versus-diagnostic behavior.

JSON bodies above the 16 MiB default fail unless callers raise max_json_response_size; callers can adjust each limit independently. A zero JSON limit accepts only empty bodies. A zero diagnostic limit omits diagnostic text rather than rejecting a non-JSON or 401/403 error response.

Existing 202/204, authentication challenge, session-expired, within-limit malformed-success-JSON, and backend-specific success-body-I/O-error behavior is preserved. An error-body I/O failure before the diagnostic limit still uses the existing read-failure placeholder (subject to the diagnostic prefix limit). This does not subsume #1208's separate malformed-JSON proposal.

Limits count bytes yielded by the HTTP backend (decompressed bytes when decompression is enabled). They bound accumulated body length, not backend/decompressor allocations, vector capacity, UTF-8 conversion, or JSON parser allocations, and are not a total RSS bound.

Validation

  • Formatting (cargo +nightly fmt --all -- --check), rustdoc (RUSTDOCFLAGS="-D warnings"), and strict Clippy (--all-targets --all-features and computed non-local feature set) pass with zero warnings.
  • Targeted integration suites (test_streamable_http_response_limits with and without reqwest/charset, test_unix_socket_response_limits, test_streamable_http_limit_lifecycle, test_streamable_http_default_error_limit, test_streamable_http_initialize_error, test_auth_error_classification, test_streamable_http_reconnect_classification) and full rmcp unit/integration suites pass.
  • Coverage includes >16 MiB base64 tool results rejected by default and accepted with an opt-in limit, unchallenged 401/403 JSON responses truncated at max_error_response_size without buffering up to max_json_response_size on both backends, old POST APIs, zero limits, EOF-free diagnostic prefixes, partial-JSON negatives, auth/session precedence, independent SSE limits, and lifecycle limit forwarding.

Prepared with AI assistance; reviewed and tested locally before submission.

@GTanger
GTanger requested a review from a team as a code owner September 5, 2026 10:35
@github-actions github-actions Bot added T-test Testing related changes T-core Core library changes T-transport Transport layer changes labels Sep 5, 2026
@DaleSeo

DaleSeo commented Oct 5, 2026

Copy link
Copy Markdown
Member

This has merge conflicts with main now. Could you rebase onto the latest main and resolve them? Thanks!

@GTanger

GTanger commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @DaleSeo! Rebased onto the latest main (8aec389) and resolved the merge conflicts in auth/streamable_http_client.rs, reqwest/streamable_http_client.rs, and unix_socket.rs.

@GTanger
GTanger force-pushed the fix/streamable-http-response-limits branch 2 times, most recently from 3e6deaa to 68f4af8 Compare October 27, 2026 14:52
@GTanger
GTanger force-pushed the fix/streamable-http-response-limits branch from 68f4af8 to b9e0ad4 Compare November 11, 2026 08:43
@GTanger

GTanger commented Nov 11, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto the latest main (f2b0ddf) and resolved the conflicts with 3e0156c (v3.9.0):

  • Preserved the v3.9.0 StreamableHttpError::ResponseBodyTooLarge { max_size } variant, common::client_side_http module layout, HTTP 5xx HttpStatusError metadata fallback, and reqwest charset/BOM error-body decoding.
  • Kept configurable StreamableHttpResponseLimits (64 MiB complete JSON default across statuses, 64 KiB non-JSON diagnostic prefix truncation via read_truncated_body to preserve legacy server/discover fallback, and 16 MiB SSE event default), Unix socket HTTP JSON-RPC error alignment, and limit forwarding across all worker POST sites and AuthClient refreshes.

Expose configurable StreamableHttpResponseLimits (defaulting to 64 MiB
for complete JSON bodies on any HTTP status, 64 KiB for HTTP diagnostic
prefixes, and 16 MiB per SSE event) while preserving the v3.9.0
ResponseBodyTooLarge { max_size } error variant, HTTP 5xx status
metadata fallback, and reqwest charset/BOM decoding.

Truncate non-JSON diagnostic error bodies without waiting for EOF so
oversized legacy 4xx pages still reach server/discover fallback, align
Unix socket JSON-RPC error handling with reqwest, and forward limits
across all worker POST sites and AuthClient token refreshes.
@GTanger
GTanger force-pushed the fix/streamable-http-response-limits branch from b9e0ad4 to 51c9290 Compare November 13, 2026 18:59
Restore StreamableHttpResponseLimits::default().max_json_response_size
to 16 MiB (matching v3.9.0) so larger JSON payloads require explicit
opt-in rather than expanding default client buffering.

Route unchallenged 401/403 JSON responses through read_truncated_body
under max_error_response_size in both reqwest and Unix socket backends
instead of buffering up to max_json_response_size before truncating to a
diagnostic prefix.
DaleSeo
DaleSeo previously approved these changes Nov 13, 2026

@DaleSeo DaleSeo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@GTanger, can you resolve the conflict? Then we can move this forward.

@DaleSeo
DaleSeo merged commit c644b75 into modelcontextprotocol:main Nov 13, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-core Core library changes T-test Testing related changes T-transport Transport layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants