Skip to content

fix: return 202 empty body for JSON-RPC notifications and reset captured response in Streamable HTTP transport - #6985

Open
wy471x wants to merge 2 commits into
apache:masterfrom
wy471x:fix_MCP-Streamable-HTTP-returns-stale-fabricated-JSON
Open

fix: return 202 empty body for JSON-RPC notifications and reset captured response in Streamable HTTP transport#6985
wy471x wants to merge 2 commits into
apache:masterfrom
wy471x:fix_MCP-Streamable-HTTP-returns-stale-fabricated-JSON

Conversation

@wy471x

@wy471x wy471x commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Make sure that:

  • You have read the contribution guidelines.
  • You submit test cases (unit or integration tests) that back your changes.
  • Your local test passed ./mvnw clean install -Dmaven.javadoc.skip=true.

Summary

Fixes #6834.

MCP Streamable HTTP notifications (notifications/initialized, notifications/cancelled) are JSON-RPC messages without an id and must be acknowledged with HTTP 202 and an empty body per the Streamable HTTP spec. Previously processWithExistingSession unconditionally chained waitForTransportResponse for every incoming message, so a notification on an existing session either replayed the stale response captured by a previous request (HTTP 200 with the wrong id, causing client mis-correlation), or received a fabricated {"jsonrpc":"2.0","result":{}} response.

Changes:

  1. ShenyuStreamableHttpServerTransportProvider.javahandleUnifiedEndpoint (POST branch): when the MessageHandlingResult has a null response body (the notification path), return the 202 ServerResponse.BodyBuilder as-is with an empty body, instead of unconditionally setting the JSON content type and writing the body.
  2. ShenyuStreamableHttpServerTransportProvider.javaprocessWithExistingSession: detect McpSchema.JSONRPCNotification messages and short-circuit — still dispatch to session.handle(...) so the MCP framework updates session state, but return 202 Accepted with a null body without invoking waitForTransportResponse. Errors on the notification path are mapped to a 500 JSON-RPC error.
  3. ShenyuStreamableHttpServerTransportProvider.javaprocessWithExistingSession (request path): call transport.resetCapturedMessage() after each completed message so a subsequent message on the same session can never observe a stale response from a previous request.

Test Cases:

  1. ShenyuStreamableHttpServerTransportProviderTesttestNotificationWithExistingSessionReturnsAcceptedEmptyBody: a notifications/cancelled notification sent on an existing session returns HTTP 202 with an empty body and the session id header.
  2. ShenyuStreamableHttpServerTransportProviderTesttestNotificationAfterRequestDoesNotReplayStaleResponse: full handshake (initialize → notifications/initializedtools/list), then a notifications/cancelled on the same session returns 202 with an empty body instead of replaying the stale tools/list response.

Verification

  • shenyu-plugin-mcp-server module: ShenyuStreamableHttpServerTransportProviderTest 4 tests passed (JDK 21).
  • Checkstyle and RAT (license header) checks passed (mvn -pl shenyu-plugin/shenyu-plugin-mcp-server -am validate).

close #6834

…red response in Streamable HTTP transport

- Detect JSONRPCNotification in processWithExistingSession and acknowledge
  with HTTP 202 empty body instead of waiting for a transport response that
  could replay the previous request's stale response or a fabricated one
- Map null response body to an empty HTTP response in handleUnifiedEndpoint
- Reset the captured transport message after each completed response so
  subsequent messages cannot observe stale state
- Add regression tests covering notifications without a prior request and
  notifications after a business request

Co-Authored-By: Claude <noreply@anthropic.com>

@Aias00 Aias00 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

processWithExistingSession now uses a null response body to represent the notification / HTTP 202 empty-body case, but only handleUnifiedEndpoint was updated to honor that contract. The gateway plugin path calls handleMessageEndpoint(exchange, request) directly and processStreamableHttpResult still serializes every result with getResponseBodyAsJson(), which turns this null into the literal response body null and writes it with the configured response headers. That means Streamable HTTP notifications routed through the real McpServerPlugin path still do not get an empty 202 response. Could you make processStreamableHttpResult return an empty response when result.getResponseBody() == null, and add a regression test through the plugin path?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] MCP Streamable-HTTP returns stale/fabricated JSON-RPC response for notifications

2 participants