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

terrymanu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shardingsphere.git


The following commit(s) were added to refs/heads/master by this push:
     new 987b54b0d42 Handle MCP servlet exceptions safely (#39732)
987b54b0d42 is described below

commit 987b54b0d42b33021a18ec981f357925a59d9fc8
Author: Liang Zhang <[email protected]>
AuthorDate: Sat Sep 5 12:30:27 2026 +0800

    Handle MCP servlet exceptions safely (#39732)
---
 .../server/http/StreamableHttpMCPServlet.java      |  9 +++
 .../server/http/StreamableHttpMCPServletTest.java  | 66 ++++++++++++++++++++++
 2 files changed, 75 insertions(+)

diff --git 
a/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/server/http/StreamableHttpMCPServlet.java
 
b/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/server/http/StreamableHttpMCPServlet.java
index f7bb40275c2..571ba12d75d 100644
--- 
a/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/server/http/StreamableHttpMCPServlet.java
+++ 
b/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/server/http/StreamableHttpMCPServlet.java
@@ -31,6 +31,7 @@ import jakarta.servlet.http.HttpServlet;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import jakarta.servlet.http.HttpServletResponseWrapper;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.shardingsphere.infra.exception.ShardingSpherePreconditions;
 import org.apache.shardingsphere.mcp.api.session.MCPSessionIdentity;
 import 
org.apache.shardingsphere.mcp.bootstrap.config.HttpTransportConfiguration;
@@ -56,6 +57,7 @@ import java.util.Objects;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.locks.StampedLock;
 
+@Slf4j
 final class StreamableHttpMCPServlet extends HttpServlet implements 
McpStreamableServerTransportProvider {
     
     private static final long serialVersionUID = -2320345528569140021L;
@@ -141,6 +143,13 @@ final class StreamableHttpMCPServlet extends HttpServlet 
implements McpStreamabl
         }
         try {
             super.service(request, response);
+        } catch (final IOException | ServletException ex) {
+            if (response.isCommitted()) {
+                throw ex;
+            }
+            log.error("Unexpected MCP HTTP request failure.", ex);
+            response.reset();
+            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
         } finally {
             lifecycleLock.unlockRead(lifecycleStamp);
         }
diff --git 
a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/server/http/StreamableHttpMCPServletTest.java
 
b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/server/http/StreamableHttpMCPServletTest.java
index 548b6b42173..f428cdde09b 100644
--- 
a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/server/http/StreamableHttpMCPServletTest.java
+++ 
b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/server/http/StreamableHttpMCPServletTest.java
@@ -58,6 +58,7 @@ import java.util.stream.Stream;
 
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.sameInstance;
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -65,6 +66,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.mockConstruction;
 import static org.mockito.Mockito.never;
@@ -198,6 +200,39 @@ class StreamableHttpMCPServletTest {
         verify(response).setCharacterEncoding("UTF-8");
     }
     
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("requestMethods")
+    void assertServiceHandleUncommittedException(final String name, final 
String requestMethod) throws IOException {
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getMethod()).thenReturn(requestMethod);
+        
when(request.getHeaderNames()).thenReturn(Collections.enumeration(List.of("Origin")));
+        
when(request.getHeaders("Origin")).thenReturn(Collections.enumeration(List.of("https://example.com";)));
+        HttpServletResponse response = mock(HttpServletResponse.class);
+        doThrow(new IOException("foo_failure")).when(response).getWriter();
+        StreamableHttpMCPServlet actual = 
createServlet(mock(MCPSessionManager.class));
+        assertDoesNotThrow(() -> actual.service(request, response));
+        verify(response).reset();
+        
verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+    }
+    
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("requestMethods")
+    void assertServiceRethrowCommittedException(final String name, final 
String requestMethod) throws IOException {
+        IOException expectedException = new IOException("foo_failure");
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getMethod()).thenReturn(requestMethod);
+        
when(request.getHeaderNames()).thenReturn(Collections.enumeration(List.of("Origin")));
+        
when(request.getHeaders("Origin")).thenReturn(Collections.enumeration(List.of("https://example.com";)));
+        HttpServletResponse response = mock(HttpServletResponse.class);
+        when(response.isCommitted()).thenReturn(true);
+        doThrow(expectedException).when(response).getWriter();
+        StreamableHttpMCPServlet actual = 
createServlet(mock(MCPSessionManager.class));
+        IOException actualException = assertThrows(IOException.class, () -> 
actual.service(request, response));
+        assertThat(actualException, sameInstance(expectedException));
+        verify(response, never()).reset();
+        verify(response, 
never()).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+    }
+    
     private static Stream<Arguments> requestMethods() {
         return Stream.of(
                 Arguments.of("get request", "GET"),
@@ -331,6 +366,22 @@ class StreamableHttpMCPServletTest {
         assertFalse(sessionManager.hasSession("session-id"));
     }
     
+    @Test
+    void assertServicePostRollbackSessionWhenDelegateFails() throws 
ServletException, IOException {
+        MCPSessionManager sessionManager = new MCPSessionManager(Map.of());
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getMethod()).thenReturn("POST");
+        
when(request.getHeaderNames()).thenReturn(Collections.emptyEnumeration());
+        HttpServletResponse response = mock(HttpServletResponse.class);
+        StreamableHttpMCPServlet actual = createServlet(sessionManager);
+        doAnswer(invocation -> {
+            ((HttpServletResponse) 
invocation.getArgument(1)).setHeader(HttpHeaders.MCP_SESSION_ID, "session-id");
+            throw new ServletException("foo_failure");
+        }).when(getDelegate()).service(any(HttpServletRequest.class), 
any(HttpServletResponse.class));
+        assertDoesNotThrow(() -> actual.service(request, response));
+        assertFalse(sessionManager.hasSession("session-id"));
+    }
+    
     @Test
     void assertServicePostWithJsonContentType() throws ServletException, 
IOException {
         HttpServletRequest request = mock(HttpServletRequest.class);
@@ -462,6 +513,21 @@ class StreamableHttpMCPServletTest {
         assertTrue(sessionManager.hasSession("session-id"));
     }
     
+    @Test
+    void assertServiceDeletePreserveSessionWhenDelegateFails() throws 
ServletException, IOException {
+        MCPSessionManager sessionManager = new 
MCPSessionManager(Collections.emptyMap());
+        sessionManager.createSession(new MCPSessionIdentity("session-id", "", 
"", Map.of()));
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getMethod()).thenReturn("DELETE");
+        
when(request.getHeaderNames()).thenReturn(Collections.emptyEnumeration());
+        
when(request.getHeader(HttpHeaders.MCP_SESSION_ID)).thenReturn("session-id");
+        HttpServletResponse response = mock(HttpServletResponse.class);
+        StreamableHttpMCPServlet actual = createServlet(sessionManager);
+        doThrow(new 
ServletException("foo_failure")).when(getDelegate()).service(any(HttpServletRequest.class),
 any(HttpServletResponse.class));
+        assertDoesNotThrow(() -> actual.service(request, response));
+        assertTrue(sessionManager.hasSession("session-id"));
+    }
+    
     @Test
     void assertCloseGracefully() {
         MCPSessionManager sessionManager = new 
MCPSessionManager(Collections.emptyMap());

Reply via email to