{"record":{"id":"9210e7ff6d3d3012","repo":"spring-projects/spring-ai","slug":"expected-mono-void-but-got-mono","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/prompt/AsyncMcpPromptListChangedMethodCallback.java","lineNumber":82,"sourceCode":"\t\t\t// Build arguments for the method call\n\t\t\tObject[] args = this.buildArgs(this.method, null, updatedPrompts);\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 McpPromptListChangedConsumerMethodException(\n\t\t\t\t\t\"Error invoking prompt 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 prompt 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/prompt/AsyncMcpPromptListChangedMethodCallback.java#L64-L100","documentation":"A ClassCastException thrown at runtime by AsyncMcpPromptListChangedMethodCallback when the async handler method returns a Mono whose element value is non-null (i.e., not Mono<Void>). Async prompt-list-changed handlers must signal completion only; a value-bearing Mono indicates the method returns actual data where none is allowed. The check wraps the result in flatMap and throws when an emitted value is observed.","triggerScenarios":"Registering an async prompt list changed handler whose method returns Mono<Prompt>, Mono<List<Prompt>>, Mono<Boolean>, etc. The error fires only when the method is invoked and the Mono emits a non-null value, not at registration time.","commonSituations":"Reusing an existing service method that returns data instead of writing a dedicated void/Mono<Void> consumer; migrating a sync handler (void) to async but keeping a return value; copy-pasting a tool-call handler that returns results into a changed-callback slot.","solutions":["Change the handler method to return Mono<Void> and end the reactive chain with .then() so nothing is emitted.","If you need the returned data elsewhere, side-effect it inside the chain (doOnNext/doOnSuccess) and still return Mono<Void>.","If the logic is synchronous, keep the method void — the async callback also accepts plain void return types."],"exampleFix":"// before\npublic Mono<List<McpSchema.Prompt>> onPromptsChanged(List<McpSchema.Prompt> prompts) {\n    return Mono.just(prompts);\n}\n// after\npublic Mono<Void> onPromptsChanged(List<McpSchema.Prompt> prompts) {\n    return refreshCache(prompts).then();\n}","handlingStrategy":"type-guard","validationCode":"Method m = handler.getClass().getMethod(\"onPromptsChanged\", List.class);\nif (!m.getReturnType().equals(Mono.class)) {\n    throw new IllegalStateException(\"async handler must return Mono<Void>\");\n}","typeGuard":"static boolean isValidAsyncHandler(Method m) {\n    Class<?> rt = m.getReturnType();\n    return rt == void.class || (Mono.class.isAssignableFrom(rt));\n}","tryCatchPattern":"try {\n    callback.accept(updatedPrompts).block();\n} catch (ClassCastException e) {\n    if (e.getMessage().startsWith(\"Expected Mono<Void>\")) {\n        log.error(\"Handler returned a value-bearing Mono; change signature to Mono<Void> and end with .then()\", e);\n    } else {\n        throw e;\n    }\n}","preventionTips":["End every reactive handler chain with .then() so nothing is emitted.","Never return Mono<T> with data from a changed-callback; side-effect via doOnNext instead.","Add an architecture test asserting handler return types at build time."],"tags":["mcp","reactor","mono-void","return-type","class-cast-exception"],"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"}