{"record":{"id":"a483fb9632b32aa8","repo":"spring-projects/spring-ai","slug":"expected-mono-void-but-got-mono-a483fb","errorCode":null,"errorMessage":"Expected Mono<Void> but got Mono<","messagePattern":"Expected Mono<Void> but got Mono<","errorType":"exception","errorClass":"ClassCastException","httpStatus":null,"severity":"error","filePath":"mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/changed/resource/AsyncMcpResourceListChangedMethodCallback.java","lineNumber":82,"sourceCode":"\t\t\t// Build arguments for the method call\n\t\t\tObject[] args = this.buildArgs(this.method, null, updatedResources);\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 McpResourceListChangedConsumerMethodException(\n\t\t\t\t\t\"Error invoking resource 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 resource list changed\n\t * consumer callback.\n\t * @param method The method to validate","sourceCodeStart":64,"sourceCodeEnd":100,"githubUrl":"https://github.com/spring-projects/spring-ai/blob/98a7beda4f29d80a71c5837eb4053b03a93a46f7/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/changed/resource/AsyncMcpResourceListChangedMethodCallback.java#L64-L100","documentation":"Async resource-list-changed callbacks may return Mono<Void>; at invocation time the library flattens the returned Mono and throws a ClassCastException if the emitted value is non-null, meaning the Mono was Mono<SomethingElse> rather than Mono<Void>. The message names the observed value's class.","triggerScenarios":"An async callback method returns Mono<String>/Mono<Boolean>/Mono<List<...>>; the value emitted at runtime is non-null inside flatMap, triggering the ClassCastException.","commonSituations":"Returning Mono.just(\"done\") or a service call that returns typed Monos instead of chaining .then() or Mono.empty() to convert to Mono<Void>.","solutions":["Convert the reactive chain to Mono<Void> by appending .then() (or .then(Mono.empty())).","Replace Mono.just(value) returns with Mono.empty().","If a value must be produced, change the method to void return type and handle the value internally."],"exampleFix":"// before\npublic Mono<String> onResourcesChanged(List<McpSchema.Resource> r) { return doWork(r); }\n// after\npublic Mono<Void> onResourcesChanged(List<McpSchema.Resource> r) { return doWork(r).then(); }","handlingStrategy":"validation","validationCode":"Method m = bean.getClass().getMethod(\"onResourcesChanged\", List.class);\nif (!Mono.class.isAssignableFrom(m.getReturnType())) throw new IllegalStateException(\"async callback must return Mono<Void>\");","typeGuard":"static boolean returnsMonoVoid(Method m) {\n    return Mono.class.isAssignableFrom(m.getReturnType());\n}","tryCatchPattern":"try {\n    callback.accept(resources).block();\n} catch (ClassCastException e) {\n    // Mono emitted a non-Void value; fix the method to end with .then()\n    log.error(\"Async callback returned Mono<T> instead of Mono<Void>: {}\", e.getMessage());\n}","preventionTips":["End every reactive callback chain with .then().","Never return Mono.just(value) from a Mono<Void> method.","Declare the return type as Mono<Void> so the compiler enforces it."],"tags":["java","mcp","reactor","classcastexception","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"}