{"record":{"id":"023c3b1d1d29edb2","repo":"spring-projects/spring-ai","slug":"mcptransportsessionclosedexception","errorCode":null,"errorMessage":"McpTransportSessionClosedException","messagePattern":"McpTransportSessionClosedException","errorType":"exception","errorClass":"McpTransportSessionClosedException","httpStatus":null,"severity":"error","filePath":"mcp/transport/mcp-spring-webflux/src/main/java/org/springframework/ai/mcp/client/webflux/transport/WebClientStreamableHttpTransport.java","lineNumber":240,"sourceCode":"\t\t\t}\n\t\t\treturn Mono.empty();\n\t\t});\n\t}\n\n\tprivate Mono<Disposable> reconnect(@Nullable McpTransportStream<Disposable> stream) {\n\t\treturn Mono.deferContextual(ctx -> {\n\t\t\tvar rh = this.handler.get();\n\t\t\tif (rh == null) {\n\t\t\t\tlogger.warn(\"Transport has no request handler registered. Remember to call connect!\");\n\t\t\t}\n\n\t\t\tfinal Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> requestHandler = rh != null\n\t\t\t\t\t? rh : msg -> Mono.error(new IllegalStateException(\"No request handler\"));\n\n\t\t\tfinal McpTransportSession<Disposable> transportSession = this.activeSession.get();\n\n\t\t\tif (ClosedMcpTransportSession.INSTANCE.equals(transportSession)) {\n\t\t\t\tthrow new McpTransportSessionClosedException();\n\t\t\t}\n\t\t\tif (stream != null) {\n\t\t\t\tif (logger.isDebugEnabled()) {\n\t\t\t\t\tlogger.debug(\"Reconnecting stream \" + stream.streamId() + \" with lastId \" + stream.lastId());\n\t\t\t\t}\n\t\t\t}\n\t\t\telse {\n\t\t\t\tlogger.debug(\"Reconnecting with no prior stream\");\n\t\t\t}\n\t\t\t// Here we attempt to initialize the client. In case the server supports SSE,\n\t\t\t// we will establish a long-running\n\t\t\t// session here and listen for messages. If it doesn't, that's ok, the server\n\t\t\t// is a simple, stateless one.\n\t\t\tfinal AtomicReference<@Nullable Disposable> disposableRef = new AtomicReference<>();\n\n\t\t\tDisposable connection = this.webClient.get()\n\t\t\t\t.uri(this.endpoint)\n\t\t\t\t.accept(MediaType.TEXT_EVENT_STREAM)","sourceCodeStart":222,"sourceCodeEnd":258,"githubUrl":"https://github.com/spring-projects/spring-ai/blob/98a7beda4f29d80a71c5837eb4053b03a93a46f7/mcp/transport/mcp-spring-webflux/src/main/java/org/springframework/ai/mcp/client/webflux/transport/WebClientStreamableHttpTransport.java#L222-L258","documentation":"WebClientStreamableHttpTransport.reconnect (called from connect and the connection loop) checks the currently active transport session before re-establishing an SSE stream. If the session reference is ClosedMcpTransportSession.INSTANCE — the session was already closed — reconnect cannot proceed and throws McpTransportSessionClosedException. This signals the transport must be fully reconnected rather than resuming an existing session.","triggerScenarios":"The streamable HTTP transport tries to reconnect a GET/SSE stream after the session was closed (server terminated session, McpTransportSessionClosedException on disconnect, or explicit close), and reconnect observes activeSession == ClosedMcpTransportSession.INSTANCE.","commonSituations":"Server restart or idle-timeout closed the MCP session; session expired per HTTP semantics (Mcp-Session-Id no longer valid); network drop followed by reconnect attempt against a stale, closed session.","solutions":["Catch McpTransportSessionClosedException and re-initialize the transport/session from scratch (call connect() again, or recreate the McpClient so a new session is negotiated).","Check server logs for 404/session-expired responses on the streamable endpoint; ensure the server keeps sessions alive or supports resumption.","Upgrade spring-ai mcp transport versions — reconnect/resume handling of closed sessions has been hardened over time."],"exampleFix":"// before: assume reconnect always resumes\ntransport.reconnect(...);\n\n// after\ntry {\n    transport.reconnect(...);\n} catch (McpTransportSessionClosedException e) {\n    mcpClient.close();          // discard closed session\n    mcpClient.initialize();     // negotiate a fresh session\n}","handlingStrategy":"retry","validationCode":"// Before reconnecting, confirm the session is still usable\nif (ClosedMcpTransportSession.INSTANCE.equals(activeSession.get())) {\n    // full re-initialization required, not a resume\n}","typeGuard":null,"tryCatchPattern":"try {\n    transport.reconnect(stream);\n} catch (McpTransportSessionClosedException e) {\n    client.close();\n    client = McpClient.sync(httpTransport).build();\n    client.initialize();\n}","preventionTips":["Handle McpTransportSessionClosedException as a signal to re-initialize, not retry blindly.","Configure keep-alives / server session timeouts to reduce premature session closure.","Monitor for HTTP 404/session-expired responses on the streamable endpoint."],"tags":["network","mcp","transport","session-closed","reconnect","webflux"],"backgroundTag":"invalid-state-transition","analyzedSha":"98a7beda4f29d80a71c5837eb4053b03a93a46f7","analyzedAt":"2026-09-11T14:15:49.441Z","contentChangedAt":"2026-09-11T14:15:49.441Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}