fix: return 202 empty body for JSON-RPC notifications and reset captured response in Streamable HTTP transport - #6985
Conversation
…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
left a comment
There was a problem hiding this comment.
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?
Make sure that:
./mvnw clean install -Dmaven.javadoc.skip=true.Summary
Fixes #6834.
MCP Streamable HTTP notifications (
notifications/initialized,notifications/cancelled) are JSON-RPC messages without anidand must be acknowledged with HTTP 202 and an empty body per the Streamable HTTP spec. PreviouslyprocessWithExistingSessionunconditionally chainedwaitForTransportResponsefor every incoming message, so a notification on an existing session either replayed the stale response captured by a previous request (HTTP 200 with the wrongid, causing client mis-correlation), or received a fabricated{"jsonrpc":"2.0","result":{}}response.Changes:
ShenyuStreamableHttpServerTransportProvider.java—handleUnifiedEndpoint(POST branch): when theMessageHandlingResulthas a null response body (the notification path), return the 202ServerResponse.BodyBuilderas-is with an empty body, instead of unconditionally setting the JSON content type and writing the body.ShenyuStreamableHttpServerTransportProvider.java—processWithExistingSession: detectMcpSchema.JSONRPCNotificationmessages and short-circuit — still dispatch tosession.handle(...)so the MCP framework updates session state, but return202 Acceptedwith a null body without invokingwaitForTransportResponse. Errors on the notification path are mapped to a 500 JSON-RPC error.ShenyuStreamableHttpServerTransportProvider.java—processWithExistingSession(request path): calltransport.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:
ShenyuStreamableHttpServerTransportProviderTest—testNotificationWithExistingSessionReturnsAcceptedEmptyBody: anotifications/cancellednotification sent on an existing session returns HTTP 202 with an empty body and the session id header.ShenyuStreamableHttpServerTransportProviderTest—testNotificationAfterRequestDoesNotReplayStaleResponse: full handshake (initialize →notifications/initialized→tools/list), then anotifications/cancelledon the same session returns 202 with an empty body instead of replaying the staletools/listresponse.Verification
shenyu-plugin-mcp-servermodule:ShenyuStreamableHttpServerTransportProviderTest4 tests passed (JDK 21).mvn -pl shenyu-plugin/shenyu-plugin-mcp-server -am validate).close #6834