spring-projects/spring-ai · error · IllegalStateException

Expected reactive return type but got: {resultClassName|null

Error message

Expected reactive return type but got: {resultClassName|null}

What it means

AbstractAsyncMcpToolMethodCallback.convertToCallToolResult throws IllegalStateException when the tool method's result is neither a Mono nor another reactive Publisher in an async callback — described in code as a fallback that 'should not happen in async context'. It means the async tool callback received a non-reactive result, indicating the method's declared return type and actual execution path disagree.

Source

Thrown at mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/tool/AbstractAsyncMcpToolMethodCallback.java:144

			// Check if the Publisher contains CallToolResult
			if (ReactiveUtils.isReactiveReturnTypeOfCallToolResult(this.toolMethod)) {
				return ((Mono<CallToolResult>) monoFromPublisher).onErrorResume(this::toErrorResultOrPropagate);
			}

			// Handle Mono<Void> for VOID return type
			if (ReactiveUtils.isReactiveReturnTypeOfVoid(this.toolMethod)) {
				return monoFromPublisher
					.then(Mono.just(CallToolResult.builder().addTextContent(jsonHelper.toJson("Done")).build()))
					.onErrorResume(this::toErrorResultOrPropagate);
			}

			// Handle other Publisher types by mapping the emitted value
			return monoFromPublisher.map(this::mapValueToCallToolResult).onErrorResume(this::toErrorResultOrPropagate);
		}

		// This should not happen in async context, but handle as fallback
		throw new IllegalStateException(
				"Expected reactive return type but got: " + (result != null ? result.getClass().getName() : "null"));
	}

	/**
	 * Map individual values to CallToolResult This method delegates to the parent class's
	 * convertValueToCallToolResult method to avoid code duplication.
	 * @param value The value to map
	 * @return A CallToolResult representing the mapped value
	 */
	protected CallToolResult mapValueToCallToolResult(Object value) {
		return convertValueToCallToolResult(value);
	}

	/**
	 * Resolves a reactive error either into an error {@link CallToolResult} conveyed to
	 * the model, or into a propagated error that fails the model interaction. Mirrors the
	 * {@code @Tool} contract: ordinary {@link RuntimeException}s are conveyed to the
	 * model, while declared checked exceptions (carried as

View on GitHub (pinned to 98a7beda4f)

Solutions

  1. Ensure the method registered on the async callback actually returns a reactive type (Mono/Flux)
  2. Remove proxies or wrappers that convert the return to a plain object before conversion
  3. Wrap the plain result: return Mono.just(value) in the tool method
  4. If the method is inherently synchronous, register it with the sync tool callback instead

Example fix

// before
String myTool(String arg) { return doWork(arg); } // registered async
// after
Mono<String> myTool(String arg) { return Mono.just(doWork(arg)); }
Defensive patterns

Strategy: type-guard

Type guard

boolean isReactiveResult(Object r) {
    return r instanceof Mono<?> || r instanceof Flux<?> || r instanceof org.reactivestreams.Publisher<?>;
}

Try / catch

try { result = asyncCallback.call(args); } catch (IllegalStateException e) { log.error("Non-reactive tool result: {}", e.getMessage()); }

Prevention

When it happens

Trigger: An async tool method declared to return a reactive type but whose invocation produced a plain object (e.g. wrapped/decorated method), or a custom subclass feeding a non-Publisher result into the async conversion path.

Common situations: Proxying/AOP around tool methods that changes the effective return type; custom async callback subclasses overriding conversion; framework version upgrades where return-type validation changed.

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/7349970929125d5a. Report an issue: GitHub.