From 235f6bde049792013b1bd137560f05b9b2450229 Mon Sep 17 00:00:00 2001 From: Daniel Garnier-Moiroux Date: Mon, 2 Nov 2026 14:31:37 +0100 Subject: [PATCH] HttpClientStreamableTransport: fix race condition on 404 response - There was a race condition where a 404 to a sendMessage call triggered client-side session closing. This would ultimately close the reactive chain representing HTTP connections, including the POST for the sendMessage itself if it had not completed by then. This would cancel the Mono instead of propagating the error. - This was a race condition because before closing the connection, the DefaultMcpTransportSession needed to send a DELETE to the server, and waited for the response. Cancelling happened only when DELETE finished before sendMessage() finished, so it would not show often. - This materialized in CI as errors for `test404WithSessionId`. Signed-off-by: Daniel Garnier-Moiroux --- .../transport/HttpClientStreamableHttpTransport.java | 7 +++++++ 1 file changed, 7 insertions(+) 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 e56a23f08..67cc1035f 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 @@ -590,6 +590,13 @@ else if (contentType.contains(APPLICATION_JSON)) { // server does not recognise is already invalidated by the time // the caller learns about it. Consumed here so that it is not // handled a second time below. + // Detached from the session first: invalidating the session + // closes it, which would otherwise dispose this very exchange and + // complete sendMessage() through doOnCancel below. + Disposable ref = disposableRef.getAndSet(null); + if (ref != null) { + transportSession.removeConnection(ref); + } handleExceptionSafely(first.getThrowable()); deliveredSink.error(first.getThrowable()); return Flux.empty();