{"record":{"id":"fb40b041178352e3","repo":"spring-projects/spring-ai","slug":"expected-mono-void-but-got-mono-value-getclas","errorCode":null,"errorMessage":"Expected Mono<Void> but got Mono<\" + value.getClass().getName() + \">","messagePattern":"Expected Mono<Void> but got Mono<\" \\+ value\\.getClass\\(\\)\\.getName\\(\\) \\+ \">","errorType":"exception","errorClass":"ClassCastException","httpStatus":null,"severity":"error","filePath":"mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/changed/tool/AsyncMcpToolListChangedMethodCallback.java","lineNumber":81,"sourceCode":"\t\t\t// Build arguments for the method call\n\t\t\tObject[] args = this.buildArgs(this.method, null, updatedTools);\n\n\t\t\t// Invoke the method\n\t\t\tthis.method.setAccessible(true);\n\t\t\tObject result = this.method.invoke(this.bean, args);\n\n\t\t\t// If the method returns a Mono, handle it\n\t\t\tif (result instanceof Mono) {\n\t\t\t\t// We need to handle the case where the Mono is not a Mono<Void>\n\t\t\t\t// This is expected by the test testInvalidMonoReturnType\n\t\t\t\tMono<?> monoResult = (Mono<?>) result;\n\n\t\t\t\t// Convert the Mono to a Mono<Void> by checking the value\n\t\t\t\t// If the value is not null (i.e., not Void), throw a ClassCastException\n\t\t\t\treturn monoResult.flatMap(value -> {\n\t\t\t\t\tif (value != null) {\n\t\t\t\t\t\t// This will be caught by the test testInvalidMonoReturnType\n\t\t\t\t\t\tthrow new ClassCastException(\n\t\t\t\t\t\t\t\t\"Expected Mono<Void> but got Mono<\" + value.getClass().getName() + \">\");\n\t\t\t\t\t}\n\t\t\t\t\treturn Mono.empty();\n\t\t\t\t}).then();\n\t\t\t}\n\t\t\t// If the method returns void, return an empty Mono\n\t\t\treturn Mono.empty();\n\t\t}\n\t\tcatch (Exception e) {\n\t\t\treturn Mono.error(new McpToolListChangedConsumerMethodException(\n\t\t\t\t\t\"Error invoking tool list changed consumer method: \" + this.method.getName(), e));\n\t\t}\n\t}\n\n\t/**\n\t * Validates that the method return type is compatible with the tool list changed\n\t * consumer callback.\n\t * @param method The method to validate","sourceCodeStart":63,"sourceCodeEnd":99,"githubUrl":"https://github.com/spring-projects/spring-ai/blob/98a7beda4f29d80a71c5837eb4053b03a93a46f7/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/changed/tool/AsyncMcpToolListChangedMethodCallback.java#L63-L99","documentation":"AsyncMcpToolListChangedMethodCallback.apply converts the handler's Mono result to Mono<Void>. If the underlying Mono emits a non-null, non-Void value, the flatMap throws ClassCastException because the contract requires the async handler to complete with Mono<Void> (no payload). The library enforces Mono<Void> at runtime even when the declared return type compiles as raw Mono.","triggerScenarios":"An async handler declared to return a raw/parameterized Mono that actually emits a value, e.g. returning Mono.just(\"done\") from a method whose declared type erases to Mono — the emitted value reaches this check and the ClassCastException is thrown inside the reactive chain (surfaced via the subscription's onError).","commonSituations":"Handlers that log-and-return Mono.just(true) or Mono.fromRunnable misuse, generic Mono<?> methods where generics were erased, or tests like testInvalidMonoReturnType intentionally returning non-Void payloads.","solutions":["Return Mono.empty() (or .then()) from the async handler instead of Mono.just(value)","If a value is needed internally, consume it in the chain and finish with .then(): monoResult.map(v -> ...).then()","Fix the declared return type to Mono<Void> so the compiler prevents emitting values"],"exampleFix":"// before\npublic Mono<?> onToolListChanged(List<McpSchema.Tool> tools) {\n    return Mono.just(processed(tools)); // emits non-Void value\n}\n// after\npublic Mono<Void> onToolListChanged(List<McpSchema.Tool> tools) {\n    return Mono.fromRunnable(() -> process(tools));\n}","handlingStrategy":"try-catch","validationCode":"if (result instanceof reactor.core.publisher.Mono<?> m) { m.hasElement().subscribe(hasValue -> { if (Boolean.TRUE.equals(hasValue)) throw new IllegalStateException(\"Handler Mono must be Mono<Void>\"); }); }","typeGuard":"static boolean isMonoVoid(java.lang.reflect.Method m) { return reactor.core.publisher.Mono.class.isAssignableFrom(m.getReturnType()); }","tryCatchPattern":"handlerResult.flatMap(v -> { if (v != null) return Mono.error(new ClassCastException(\"Expected Mono<Void> but got \" + v.getClass().getName())); return Mono.empty(); }).onErrorResume(ClassCastException.class, e -> { log.error(e.getMessage()); return Mono.empty(); }).then();","preventionTips":["Declare handlers as Mono<Void> so the compiler prevents emitting values","End reactive chains with .then() or use Mono.fromRunnable for side-effect-only work","Never return Mono.just(value) from a list-changed handler"],"tags":["java","reactor","type-mismatch","mono"],"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"}