{"record":{"id":"88c95e620721ec9f","repo":"spring-projects/spring-ai","slug":"unsupported-async-exchange-type-exchangeclassnam-88c95e","errorCode":null,"errorMessage":"Unsupported Async exchange type: {exchangeClassName} for Sync method: {methodName} in {className}","messagePattern":"Unsupported Async exchange type: (.+?) for Sync method: (.+?) in (.+?)","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/resource/SyncStatelessMcpResourceMethodCallback.java","lineNumber":82,"sourceCode":"\t\t\tthrow new IllegalArgumentException(\n\t\t\t\t\t\"Stateless Streamable-Http prompt method must not declare parameter of type: \" + paramType.getName()\n\t\t\t\t\t\t\t+ \". Use McpTransportContext instead.\" + \" Method: \" + this.method.getName() + \" in \"\n\t\t\t\t\t\t\t+ this.method.getDeclaringClass().getName());\n\t\t}\n\t}\n\n\t@Override\n\tprotected Object assignExchangeType(Class<?> paramType, Object exchange) {\n\n\t\tif (McpTransportContext.class.isAssignableFrom(paramType)) {\n\t\t\tif (exchange instanceof McpTransportContext transportContext) {\n\t\t\t\treturn transportContext;\n\t\t\t}\n\t\t\telse if (exchange instanceof McpSyncServerExchange syncServerExchange) {\n\t\t\t\treturn syncServerExchange.transportContext();\n\t\t\t}\n\t\t\telse if (exchange instanceof McpAsyncServerExchange asyncServerExchange) {\n\t\t\t\tthrow new IllegalArgumentException(\"Unsupported Async exchange type: \"\n\t\t\t\t\t\t+ asyncServerExchange.getClass().getName() + \" for Sync method: \" + method.getName() + \" in \"\n\t\t\t\t\t\t+ method.getDeclaringClass().getName());\n\t\t\t}\n\t\t}\n\n\t\tthrow new IllegalArgumentException(\n\t\t\t\t\"Unsupported exchange type: \" + (exchange != null ? exchange.getClass().getName() : \"null\")\n\t\t\t\t\t\t+ \" for method: \" + method.getName() + \" in \" + method.getDeclaringClass().getName());\n\t}\n\n\t/**\n\t * Apply the callback to the given context and request.\n\t * <p>\n\t * This method extracts URI variable values from the request URI, builds the arguments\n\t * for the method call, invokes the method, and converts the result to a\n\t * ReadResourceResult.\n\t * @param context The transport context, may be null if the method doesn't require it\n\t * @param request The resource request, must not be null","sourceCodeStart":64,"sourceCodeEnd":100,"githubUrl":"https://github.com/spring-projects/spring-ai/blob/98a7beda4f29d80a71c5837eb4053b03a93a46f7/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/resource/SyncStatelessMcpResourceMethodCallback.java#L64-L100","documentation":"In SyncStatelessMcpResourceMethodCallback.assignExchangeType, when the method declares a McpTransportContext parameter and the runtime exchange argument happens to be an McpAsyncServerExchange, the callback cannot derive a transport context for a sync invocation and throws IllegalArgumentException. It mirrors error 232 but for the stateless callback.","triggerScenarios":"A stateless sync resource method expecting McpTransportContext receives an McpAsyncServerExchange as its exchange argument — e.g., the stateless handler is registered/invoked through an async dispatcher.","commonSituations":"Sharing one callback between stateful async and stateless sync pipelines, or test code constructing an async exchange and passing it to a stateless sync callback.","solutions":["Register the method only with the stateless sync server so the runtime exchange is not an async exchange.","Pass a McpTransportContext (or sync exchange whose transportContext() can be used) instead of an async exchange.","Split the handler: async path uses McpAsyncServerExchange, stateless path uses McpTransportContext."],"exampleFix":"// before\nstatelessServer.addResource(..., (McpAsyncServerExchange ex, req) -> callback.apply(ex, req));\n\n// after\nstatelessServer.addResource(..., (McpTransportContext ctx, req) -> callback.apply(ctx, req));","handlingStrategy":"type-guard","validationCode":"Object safeContextForStateless(Object exchange) {\n    if (exchange instanceof McpAsyncServerExchange)\n        throw new IllegalArgumentException(\"Async exchange passed to stateless sync callback\");\n    return exchange;\n}","typeGuard":"static boolean isStatelessCompatible(Object exchange) {\n    return exchange instanceof McpTransportContext\n        || (exchange instanceof McpSyncServerExchange);\n}","tryCatchPattern":"try {\n    return callback.apply(exchange, request);\n} catch (IllegalArgumentException e) {\n    if (e.getMessage().startsWith(\"Unsupported Async exchange type\")) {\n        log.error(\"Stateless sync handler invoked with async exchange; fix dispatcher wiring\", e);\n        throw new IllegalStateException(\"Stateless/async wiring mismatch\", e);\n    } throw e;\n}","preventionTips":["Never reuse stateful async dispatch code to drive stateless sync callbacks.","Assert the exchange type at dispatcher boundaries before invoking callbacks.","Cover stateless handlers with integration tests through the stateless server API."],"tags":["mcp","stateless","sync-async-mismatch","exchange-type","illegal-argument"],"backgroundTag":"type-mismatch","analyzedSha":"98a7beda4f29d80a71c5837eb4053b03a93a46f7","analyzedAt":"2026-09-11T14:15:49.441Z","contentChangedAt":"2026-09-11T14:15:49.441Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}