spring-projects/spring-ai · error · IllegalArgumentException
Unsupported Async exchange type: {exchangeClassName} for Syn
Error message
Unsupported Async exchange type: {exchangeClassName} for Sync method: {methodName} in {className} What it means
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.
Source
Thrown at mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/resource/SyncStatelessMcpResourceMethodCallback.java:82
throw new IllegalArgumentException(
"Stateless Streamable-Http prompt method must not declare parameter of type: " + paramType.getName()
+ ". Use McpTransportContext instead." + " Method: " + this.method.getName() + " in "
+ this.method.getDeclaringClass().getName());
}
}
@Override
protected Object assignExchangeType(Class<?> paramType, Object exchange) {
if (McpTransportContext.class.isAssignableFrom(paramType)) {
if (exchange instanceof McpTransportContext transportContext) {
return transportContext;
}
else if (exchange instanceof McpSyncServerExchange syncServerExchange) {
return syncServerExchange.transportContext();
}
else if (exchange instanceof McpAsyncServerExchange asyncServerExchange) {
throw new IllegalArgumentException("Unsupported Async exchange type: "
+ asyncServerExchange.getClass().getName() + " for Sync method: " + method.getName() + " in "
+ method.getDeclaringClass().getName());
}
}
throw new IllegalArgumentException(
"Unsupported exchange type: " + (exchange != null ? exchange.getClass().getName() : "null")
+ " for method: " + method.getName() + " in " + method.getDeclaringClass().getName());
}
/**
* Apply the callback to the given context and request.
* <p>
* This method extracts URI variable values from the request URI, builds the arguments
* for the method call, invokes the method, and converts the result to a
* ReadResourceResult.
* @param context The transport context, may be null if the method doesn't require it
* @param request The resource request, must not be nullView on GitHub (pinned to 98a7beda4f)
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.
Example fix
// before statelessServer.addResource(..., (McpAsyncServerExchange ex, req) -> callback.apply(ex, req)); // after statelessServer.addResource(..., (McpTransportContext ctx, req) -> callback.apply(ctx, req));
Defensive patterns
Strategy: type-guard
Validate before calling
Object safeContextForStateless(Object exchange) {
if (exchange instanceof McpAsyncServerExchange)
throw new IllegalArgumentException("Async exchange passed to stateless sync callback");
return exchange;
} Type guard
static boolean isStatelessCompatible(Object exchange) {
return exchange instanceof McpTransportContext
|| (exchange instanceof McpSyncServerExchange);
} Try / catch
try {
return callback.apply(exchange, request);
} catch (IllegalArgumentException e) {
if (e.getMessage().startsWith("Unsupported Async exchange type")) {
log.error("Stateless sync handler invoked with async exchange; fix dispatcher wiring", e);
throw new IllegalStateException("Stateless/async wiring mismatch", e);
} throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.
Related errors
- Unsupported Async exchange type: {exchangeClassName} for Syn
- Unsupported Async exchange type: {syncServerExchange.getClas
- Unsupported Sync exchange type: {syncServerExchange.getClass
- Unsupported Async exchange type: {asyncServerExchange.getCla
- Unsupported Async exchange type: ${exchangeType} for Sync me
AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11).
Data as JSON: /api/errors/88c95e620721ec9f.
Report an issue: GitHub.