From 3c200d3d335b6c853a62169968fe63a9e20bea83 Mon Sep 17 00:00:00 2001 From: jiajingda Date: Thu, 9 Apr 2026 09:49:06 +0800 Subject: [PATCH] Fix SSE event classification to follow spec for missing event field Per the SSE specification (WHATWG HTML Living Standard 9.2.6), an event with no explicit event field MUST be dispatched as a message event. HttpClientStreamableHttpTransport previously used strict equality and silently dropped such frames in the reconnect/GET stream path, causing server-initiated notifications to never reach the handler. Extract classification into a package-private isMessageEvent helper and cover with parameterized unit tests. Closes gh-885 --- .../HttpClientStreamableHttpTransport.java | 25 ++++++++- ...reamableHttpTransportSseEventTypeTest.java | 51 +++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 mcp-core/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportSseEventTypeTest.java diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java index 142c0302c..fb6ed3327 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java @@ -112,6 +112,29 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport { public static int BAD_REQUEST = 400; + /** + * Determines whether an SSE event should be treated as a "message" event carrying a + * JSON-RPC payload. + * + *

+ * Per the + * SSE specification (WHATWG HTML Living Standard §9.2.6), an event with no + * explicit {@code event:} field MUST be dispatched as a {@code message} event by + * default. This method applies that rule by treating {@code null} or empty event + * names as equivalent to {@link #MESSAGE_EVENT_TYPE}. + * + *

+ * This alignment ensures interoperability with MCP servers that emit bare + * {@code data:} frames without an accompanying {@code event:} line, which are valid + * per the SSE spec. + * @param eventName the SSE event name, which may be {@code null} or empty + * @return {@code true} if the event should be parsed as a JSON-RPC message + */ + static boolean isMessageEvent(String eventName) { + return eventName == null || eventName.isEmpty() || MESSAGE_EVENT_TYPE.equals(eventName); + } + private final McpJsonMapper jsonMapper; private final URI baseUri; @@ -311,7 +334,7 @@ else if (statusCode == METHOD_NOT_ALLOWED) { + statusCode)); } else if (statusCode >= 200 && statusCode < 300) { - if (MESSAGE_EVENT_TYPE.equals(sseResponseEvent.sseEvent().event())) { + if (isMessageEvent(sseResponseEvent.sseEvent().event())) { String data = sseResponseEvent.sseEvent().data(); // Per 2025-11-25 spec (SEP-1699), servers may // send SSE events diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportSseEventTypeTest.java b/mcp-core/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportSseEventTypeTest.java new file mode 100644 index 000000000..d5f7196bd --- /dev/null +++ b/mcp-core/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportSseEventTypeTest.java @@ -0,0 +1,51 @@ +/* + * Copyright 2024-2026 the original author or authors. + */ + +package io.modelcontextprotocol.client.transport; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link HttpClientStreamableHttpTransport#isMessageEvent(String)}. + * + *

+ * Verifies that SSE event classification follows the + * WHATWG HTML Living Standard §9.2.6: an event without an explicit {@code event:} + * field must be dispatched as a {@code message} event. + * + * @author jiajingda + * @see #885 + */ +class HttpClientStreamableHttpTransportSseEventTypeTest { + + @ParameterizedTest + @NullAndEmptySource + void shouldTreatNullOrEmptyEventAsMessage(String eventName) { + assertThat(HttpClientStreamableHttpTransport.isMessageEvent(eventName)) + .as("SSE frame with null/empty event field must be treated as a 'message' event per SSE spec") + .isTrue(); + } + + @Test + void shouldTreatExplicitMessageEventAsMessage() { + assertThat(HttpClientStreamableHttpTransport.isMessageEvent("message")) + .as("Explicit 'message' event must be parsed as a JSON-RPC message") + .isTrue(); + } + + @ParameterizedTest + @ValueSource(strings = { "ping", "error", "notification", "MESSAGE", "Message", "custom-event" }) + void shouldNotTreatOtherEventsAsMessage(String eventName) { + assertThat(HttpClientStreamableHttpTransport.isMessageEvent(eventName)) + .as("Non-'message' SSE event '%s' must not be parsed as a JSON-RPC message", eventName) + .isFalse(); + } + +} \ No newline at end of file