This is an automated email from the ASF dual-hosted git repository.
Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git
The following commit(s) were added to refs/heads/master by this push:
new 5fda45cc45 [type: bug] Fix session key mismatch in streamable HTTP
transport (#6833) (#6997)
5fda45cc45 is described below
commit 5fda45cc45fb5386309db23d9f329639caf1e2ea
Author: Arvin <[email protected]>
AuthorDate: Sun Oct 11 23:20:53 2026 +0800
[type: bug] Fix session key mismatch in streamable HTTP transport (#6833)
(#6997)
---
...henyuStreamableHttpServerTransportProvider.java | 16 +++++++-
...uStreamableHttpServerTransportProviderTest.java | 45 ++++++++++++++++++++++
2 files changed, 60 insertions(+), 1 deletion(-)
diff --git
a/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuStreamableHttpServerTransportProvider.java
b/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuStreamableHttpServerTransportProvider.java
index 344257f912..ba356c2a14 100644
---
a/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuStreamableHttpServerTransportProvider.java
+++
b/shenyu-plugin/shenyu-plugin-mcp-server/src/main/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuStreamableHttpServerTransportProvider.java
@@ -332,6 +332,7 @@ public class ShenyuStreamableHttpServerTransportProvider
implements McpServerTra
final StreamableHttpSessionTransport transport = new
StreamableHttpSessionTransport();
final McpServerSession session = sessionFactory.create(transport);
final String newSessionId = session.getId();
+ transport.setSessionId(newSessionId);
LOGGER.debug("Created new MCP session: {}", newSessionId);
// Store session and transport for reuse
sessions.put(newSessionId, session);
@@ -1054,7 +1055,7 @@ public class ShenyuStreamableHttpServerTransportProvider
implements McpServerTra
*/
private class StreamableHttpSessionTransport implements McpServerTransport
{
- private final String sessionId;
+ private String sessionId;
private volatile boolean closed;
@@ -1082,6 +1083,19 @@ public class ShenyuStreamableHttpServerTransportProvider
implements McpServerTra
LOGGER.debug("Created StreamableHttpSessionTransport with
sessionId: {}", this.sessionId);
}
+ /**
+ * Sets the session ID to match the MCP server session's ID.
+ * This ensures the transport's sessionId is consistent with the key
used
+ * to store the session in {@code sessions} and {@code
sessionTransports},
+ * so that {@link #close()} and {@link #closeGracefully()} correctly
+ * remove the session.
+ *
+ * @param sessionId the session identifier to use
+ */
+ public void setSessionId(final String sessionId) {
+ this.sessionId = sessionId;
+ }
+
/**
* Gets the last message sent through this transport.
* This is used for Streamable HTTP response correlation, allowing the
diff --git
a/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuStreamableHttpServerTransportProviderTest.java
b/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuStreamableHttpServerTransportProviderTest.java
index f2b160e5ac..e73ccae0dc 100644
---
a/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuStreamableHttpServerTransportProviderTest.java
+++
b/shenyu-plugin/shenyu-plugin-mcp-server/src/test/java/org/apache/shenyu/plugin/mcp/server/transport/ShenyuStreamableHttpServerTransportProviderTest.java
@@ -20,6 +20,7 @@ package org.apache.shenyu.plugin.mcp.server.transport;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpServerSession;
+import io.modelcontextprotocol.spec.McpServerTransport;
import io.modelcontextprotocol.server.McpRequestHandler;
import org.apache.shenyu.plugin.mcp.server.session.McpSessionHelper;
import org.springframework.web.server.ServerWebExchange;
@@ -229,6 +230,50 @@ class ShenyuStreamableHttpServerTransportProviderTest {
provider.removeSession(sessionId);
}
+ /**
+ * Regression test for the reported initialize-path session leak (#6833).
+ *
+ * <p>Before the fix the provider stored the session under the MCP server
session ID while the
+ * transport kept its own independently auto-generated {@code sessionId},
so {@code close()}
+ * looked up a key that never existed in {@code sessions} / {@code
sessionTransports} and left
+ * both registries plus {@link ShenyuMcpExchangeHolder} populated forever.
+ *
+ * <p>Assertion: after a real initialize handshake the ID returned to the
client
+ * ({@code Mcp-Session-Id}) is exactly the key used in both registries, so
a later
+ * {@code close()} is able to remove the entry.
+ */
+ @Test
+ void testInitializeRegistersSessionUnderReturnedSessionId() throws
Exception {
+ ShenyuStreamableHttpServerTransportProvider provider =
providerWithRealSessions();
+
+ MockServerHttpResponse response = performRequest(provider,
+ postRequest(INITIALIZE_REQUEST_BODY, null));
+ assertEquals(HttpStatus.OK, response.getStatusCode());
+
+ final String returnedSessionId =
response.getHeaders().getFirst(SESSION_ID_HEADER);
+ assertNotNull(returnedSessionId);
+
+ final Map<String, ?> sessions = readMap(provider, "sessions");
+ final Map<String, ?> transports = readMap(provider,
"sessionTransports");
+ assertTrue(sessions.containsKey(returnedSessionId),
+ "sessions must be keyed by the session ID returned to the
client, otherwise close() cannot clean it up");
+ assertTrue(transports.containsKey(returnedSessionId),
+ "sessionTransports must be keyed by the session ID returned to
the client, otherwise close() cannot clean it up");
+
+ // Drive the close path: it is the only thing that exercises the
transport's own sessionId,
+ // so without the setSessionId() fix nothing would be removed here.
Asserting only the map
+ // keys above would pass on master too (they were always keyed by the
returned id); this part
+ // fails when line transport.setSessionId(newSessionId) is reverted.
+ final McpServerTransport transport = (McpServerTransport)
transports.get(returnedSessionId);
+ assertNotNull(transport, "the transport must be reachable via the
returned session id");
+ transport.closeGracefully().block();
+
+ assertTrue(sessions.isEmpty(), "close() must remove the session from
the sessions map");
+ assertTrue(transports.isEmpty(), "close() must remove the transport
from the sessionTransports map");
+ assertNull(ShenyuMcpExchangeHolder.get(returnedSessionId),
+ "close() must remove the exchange binding for the returned
session id");
+ }
+
@SuppressWarnings("unchecked")
private Map<String, ?> readMap(final
ShenyuStreamableHttpServerTransportProvider provider, final String fieldName)
throws Exception {