{"record":{"id":"7349970929125d5a","repo":"spring-projects/spring-ai","slug":"expected-reactive-return-type-but-got-resultclas","errorCode":null,"errorMessage":"Expected reactive return type but got: {resultClassName|null}","messagePattern":"Expected reactive return type but got: (.+?)","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/tool/AbstractAsyncMcpToolMethodCallback.java","lineNumber":144,"sourceCode":"\n\t\t\t// Check if the Publisher contains CallToolResult\n\t\t\tif (ReactiveUtils.isReactiveReturnTypeOfCallToolResult(this.toolMethod)) {\n\t\t\t\treturn ((Mono<CallToolResult>) monoFromPublisher).onErrorResume(this::toErrorResultOrPropagate);\n\t\t\t}\n\n\t\t\t// Handle Mono<Void> for VOID return type\n\t\t\tif (ReactiveUtils.isReactiveReturnTypeOfVoid(this.toolMethod)) {\n\t\t\t\treturn monoFromPublisher\n\t\t\t\t\t.then(Mono.just(CallToolResult.builder().addTextContent(jsonHelper.toJson(\"Done\")).build()))\n\t\t\t\t\t.onErrorResume(this::toErrorResultOrPropagate);\n\t\t\t}\n\n\t\t\t// Handle other Publisher types by mapping the emitted value\n\t\t\treturn monoFromPublisher.map(this::mapValueToCallToolResult).onErrorResume(this::toErrorResultOrPropagate);\n\t\t}\n\n\t\t// This should not happen in async context, but handle as fallback\n\t\tthrow new IllegalStateException(\n\t\t\t\t\"Expected reactive return type but got: \" + (result != null ? result.getClass().getName() : \"null\"));\n\t}\n\n\t/**\n\t * Map individual values to CallToolResult This method delegates to the parent class's\n\t * convertValueToCallToolResult method to avoid code duplication.\n\t * @param value The value to map\n\t * @return A CallToolResult representing the mapped value\n\t */\n\tprotected CallToolResult mapValueToCallToolResult(Object value) {\n\t\treturn convertValueToCallToolResult(value);\n\t}\n\n\t/**\n\t * Resolves a reactive error either into an error {@link CallToolResult} conveyed to\n\t * the model, or into a propagated error that fails the model interaction. Mirrors the\n\t * {@code @Tool} contract: ordinary {@link RuntimeException}s are conveyed to the\n\t * model, while declared checked exceptions (carried as","sourceCodeStart":126,"sourceCodeEnd":162,"githubUrl":"https://github.com/spring-projects/spring-ai/blob/98a7beda4f29d80a71c5837eb4053b03a93a46f7/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/tool/AbstractAsyncMcpToolMethodCallback.java#L126-L162","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Ensure the method registered on the async callback actually returns a reactive type (Mono/Flux)","Remove proxies or wrappers that convert the return to a plain object before conversion","Wrap the plain result: return Mono.just(value) in the tool method","If the method is inherently synchronous, register it with the sync tool callback instead"],"exampleFix":"// before\nString myTool(String arg) { return doWork(arg); } // registered async\n// after\nMono<String> myTool(String arg) { return Mono.just(doWork(arg)); }","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"boolean isReactiveResult(Object r) {\n    return r instanceof Mono<?> || r instanceof Flux<?> || r instanceof org.reactivestreams.Publisher<?>;\n}","tryCatchPattern":"try { result = asyncCallback.call(args); } catch (IllegalStateException e) { log.error(\"Non-reactive tool result: {}\", e.getMessage()); }","preventionTips":["Ensure async tool methods always return Publisher types end to end","Beware AOP proxies that wrap and unwrap reactive return values","Keep sync methods registered with sync callbacks"],"tags":["reactive","tool","return-type"],"backgroundTag":"type-mismatch","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"}