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 null

View on GitHub (pinned to 98a7beda4f)

Solutions

  1. Register the method only with the stateless sync server so the runtime exchange is not an async exchange.
  2. Pass a McpTransportContext (or sync exchange whose transportContext() can be used) instead of an async exchange.
  3. 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

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


AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11). Data as JSON: /api/errors/88c95e620721ec9f. Report an issue: GitHub.