This is an automated email from the ASF dual-hosted git repository.

dengliming 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 d396e0fc1e fix: clean up restore-created mcp sessions after request to 
prevent memory leak (#6644) (#7039)
d396e0fc1e is described below

commit d396e0fc1e698f5d5a684e0221e6d15eb4eacd4f
Author: wy471x <[email protected]>
AuthorDate: Thu Sep 17 16:28:33 2026 +0800

    fix: clean up restore-created mcp sessions after request to prevent memory 
leak (#6644) (#7039)
    
    Add a doFinally cleanup hook to createSessionAndRestoreId so the 
McpServerSession, StreamableHttpSessionTransport, and ShenyuMcpExchangeHolder 
entry created for an unknown/stale sessionId are removed after the one-shot 
request completes, mirroring createTemporarySessionAndProcess. Previously these 
entries stayed in sessions/sessionTransports forever, allowing unbounded memory 
growth.
    
    Adds testStaleSessionRestoreCleansUpCreatedSession to verify the 
restore-created session, transport, and exchange mapping are removed and that 
follow-up requests are handled by a fresh restore.
    
    close #6644
    
    Co-authored-by: Liming Deng <[email protected]>
---
 ...henyuStreamableHttpServerTransportProvider.java | 10 ++++--
 ...uStreamableHttpServerTransportProviderTest.java | 40 ++++++++++++++++++++++
 2 files changed, 48 insertions(+), 2 deletions(-)

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 3ba4371c63..8773277d3d 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
@@ -452,10 +452,11 @@ public class ShenyuStreamableHttpServerTransportProvider 
implements McpServerTra
      * This method handles scenarios where a client provides a session ID that 
no longer
      * exists on the server (e.g., server restart, session timeout, network 
disconnection).
      * A new session is created using the MCP framework, which generates its 
own session ID.
-     * The client receives the new session ID for subsequent requests.
+     * The session is only used to process the current request and is cleaned 
up afterwards,
+     * so an unknown or stale session ID cannot leave orphaned sessions in the 
maps.
      * Important: The MCP framework generates its own session IDs, so the
      * client's requested session ID may differ from the actual session ID 
returned.
-     * The response includes the actual session ID that should be used for 
future requests.
+     * The response includes the actual session ID used to process this 
request.
      *
      * @param exchange           the server web exchange
      * @param message            the JSON-RPC message
@@ -479,6 +480,11 @@ public class ShenyuStreamableHttpServerTransportProvider 
implements McpServerTra
             initializeSessionDirectly(newSession, actualSessionId);
             newTransport.resetCapturedMessage();
             return processWithExistingSession(newSession, actualSessionId, 
message, messageId)
+                    .doFinally(signalType -> {
+                        LOGGER.debug("Cleaning up restored session: {} 
(signal: {})", actualSessionId, signalType);
+                        removeSession(actualSessionId);
+                        ShenyuMcpExchangeHolder.remove(actualSessionId);
+                    })
                     .map(result -> {
                         if (!actualSessionId.equals(requestedSessionId)) {
                             LOGGER.info("Returning actual session ID {} 
instead of requested ID {}", actualSessionId, requestedSessionId);
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 7d3bddb70b..2e17944c90 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
@@ -36,6 +36,7 @@ import 
org.springframework.web.reactive.result.view.ViewResolver;
 import reactor.core.publisher.Mono;
 import reactor.test.StepVerifier;
 
+import java.lang.reflect.Field;
 import java.time.Duration;
 import java.util.Collections;
 import java.util.List;
@@ -45,7 +46,9 @@ import java.util.Objects;
 import java.util.UUID;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
@@ -172,6 +175,43 @@ class ShenyuStreamableHttpServerTransportProviderTest {
         assertEquals("", notificationResponse.getBodyAsString().block());
     }
 
+    /**
+     * A request carrying a session ID that no longer exists on the server 
must not
+     * leave the restore-created session, transport, or exchange mapping 
behind after
+     * the request completes.
+     */
+    @Test
+    void testStaleSessionRestoreCleansUpCreatedSession() throws Exception {
+        ShenyuStreamableHttpServerTransportProvider provider = 
providerWithRealSessions();
+
+        MockServerHttpResponse response = performRequest(provider, 
postRequest(TOOLS_LIST_REQUEST_BODY, "stale-session-1"));
+        assertEquals(HttpStatus.OK, response.getStatusCode());
+        assertTrue(response.getBodyAsString().block().contains("\"tools\""));
+        final String actualSessionId = 
response.getHeaders().getFirst(SESSION_ID_HEADER);
+        assertNotNull(actualSessionId);
+
+        assertEquals(0, readMap(provider, "sessions").size());
+        assertEquals(0, readMap(provider, "sessionTransports").size());
+        assertNull(ShenyuMcpExchangeHolder.get(actualSessionId));
+
+        // The restored session is not reusable, so a follow-up request with 
the
+        // returned session ID is handled by a fresh restore instead of the 
same session.
+        MockServerHttpResponse followUp = performRequest(provider, 
postRequest(TOOLS_LIST_REQUEST_BODY, actualSessionId));
+        assertEquals(HttpStatus.OK, followUp.getStatusCode());
+        assertTrue(followUp.getBodyAsString().block().contains("\"tools\""));
+        assertNotEquals(actualSessionId, 
followUp.getHeaders().getFirst(SESSION_ID_HEADER));
+        assertEquals(0, readMap(provider, "sessions").size());
+        assertEquals(0, readMap(provider, "sessionTransports").size());
+    }
+
+    @SuppressWarnings("unchecked")
+    private Map<String, ?> readMap(final 
ShenyuStreamableHttpServerTransportProvider provider, final String fieldName)
+            throws Exception {
+        final Field field = provider.getClass().getDeclaredField(fieldName);
+        field.setAccessible(true);
+        return (Map<String, ?>) field.get(provider);
+    }
+
     private ShenyuStreamableHttpServerTransportProvider 
providerWithRealSessions() {
         ShenyuStreamableHttpServerTransportProvider provider =
                 new ShenyuStreamableHttpServerTransportProvider(new 
ObjectMapper(), "/mcp/streamablehttp");

Reply via email to