From b325ace196c99fb91652dccee3532c1e208ebea3 Mon Sep 17 00:00:00 2001 From: Daniel Garnier-Moiroux Date: Thu, 2 Apr 2026 15:41:07 +0200 Subject: [PATCH] HttpClientStreamableHttpTransport: handle HTTP 405 - Fixes #877 Signed-off-by: Daniel Garnier-Moiroux --- .../HttpClientStreamableHttpTransport.java | 8 +++---- ...eamableHttpTransportErrorHandlingTest.java | 22 ++++++++++++++++++- 2 files changed, 25 insertions(+), 5 deletions(-) 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 9e9b7f923..4c0658603 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 @@ -298,6 +298,10 @@ private Mono reconnect(McpTransportStream stream) { "Authorization error connecting to SSE stream", responseEvent.responseInfo())); } + else if (statusCode == METHOD_NOT_ALLOWED) { + logger.debug("The server does not support SSE streams, using request-response mode."); + return Flux.empty(); + } if (!(responseEvent instanceof ResponseSubscribers.SseResponseEvent sseResponseEvent)) { return Flux.error(new McpTransportException( @@ -344,10 +348,6 @@ else if (statusCode >= 200 && statusCode < 300) { return Flux.empty(); } } - else if (statusCode == METHOD_NOT_ALLOWED) { // NotAllowed - logger.debug("The server does not support SSE streams, using request-response mode."); - return Flux.empty(); - } else if (statusCode == NOT_FOUND) { if (transportSession != null && transportSession.sessionId().isPresent()) { diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportErrorHandlingTest.java b/mcp-test/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportErrorHandlingTest.java index c4857e5b4..d3793ca01 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportErrorHandlingTest.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransportErrorHandlingTest.java @@ -18,7 +18,6 @@ import com.sun.net.httpserver.HttpServer; import io.modelcontextprotocol.client.transport.customizer.McpHttpClientAuthorizationErrorHandler; import io.modelcontextprotocol.common.McpTransportContext; -import org.reactivestreams.Publisher; import io.modelcontextprotocol.server.transport.TomcatTestUtil; import io.modelcontextprotocol.spec.HttpHeaders; import io.modelcontextprotocol.spec.McpClientTransport; @@ -34,6 +33,7 @@ import org.junit.jupiter.api.Timeout; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -369,6 +369,26 @@ else if (status == 404) { StepVerifier.create(transport.closeGracefully()).verifyComplete(); } + @Test + void test405OnConnectReturnsEmptyFlux() { + serverSseResponseStatus.set(405); + AtomicReference capturedException = new AtomicReference<>(); + var transport = HttpClientStreamableHttpTransport.builder(HOST).openConnectionOnStartup(true).build(); + transport.setExceptionHandler(capturedException::set); + + var messages = new ArrayList(); + StepVerifier.create(transport.connect(msg -> msg.doOnNext(messages::add))).verifyComplete(); + + Awaitility.await() + .atMost(Duration.ofSeconds(1)) + .untilAsserted(() -> assertThat(processedSseConnectCount.get()).isEqualTo(1)); + + assertThat(messages).isEmpty(); + assertThat(capturedException.get()).isNull(); + + StepVerifier.create(transport.closeGracefully()).verifyComplete(); + } + @Nested class AuthorizationError {