{"record":{"id":"6b213b424327981f","repo":"spring-projects/spring-ai","slug":"expected-mono-void-but-got-mono-6b213b","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/logging/AsyncMcpLoggingMethodCallback.java","lineNumber":80,"sourceCode":"\t\t\t// Build arguments for the method call\n\t\t\tObject[] args = this.buildArgs(this.method, null, notification);\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 McpLoggingConsumerMethodException(\n\t\t\t\t\t\"Error invoking logging 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 logging consumer\n\t * callback.\n\t * @param method The method to validate","sourceCodeStart":62,"sourceCodeEnd":98,"githubUrl":"https://github.com/spring-projects/spring-ai/blob/98a7beda4f29d80a71c5837eb4053b03a93a46f7/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/logging/AsyncMcpLoggingMethodCallback.java#L62-L98","documentation":"An @McpLogging annotated method declared to return Mono<Void> actually emitted a non-null, non-Void value. The callback flatMaps over the result and throws ClassCastException naming the unexpected element type, because a logging handler must complete without emitting a value. The error message is truncated at 'Mono<' plus the value's class name.","triggerScenarios":"Registering an async logging handler method whose declared return type is Mono<Void> but whose reactive chain ends with mono.just(something) or map(...) producing a non-null value, then the framework invoking apply() when a logging notification arrives.","commonSituations":"Returning Mono.just(true) or a mapped result from a logging handler out of habit from other reactive code; a refactor that changed the handler's last operator from then()/empty() to map(); misunderstanding that Mono<Void> means the pipeline must never emit an element.","solutions":["End the handler's reactive chain with .then() or return Mono.empty() so no value is emitted.","Replace map(...) with flatMap(... -> Mono.empty()) or thenMap-free operators that discard the element.","If you actually need to return data, the method is not a logging consumer — move the logic elsewhere or change the annotation/callback type.","Check the test testInvalidMonoReturnType pattern: verify your handler with a unit test that subscribes and asserts the Mono completes empty."],"exampleFix":"// before\npublic Mono<Void> handleLog(LoggingMessageNotification n) {\n    return Mono.just(log(n));\n}\n// after\npublic Mono<Void> handleLog(LoggingMessageNotification n) {\n    return Mono.fromRunnable(() -> log(n));\n}","handlingStrategy":"validation","validationCode":"Mono<Void> result = handler.apply(notification);\n// verify emptiness before trusting the handler\nresult.subscribe(v -> { if (v != null) throw new IllegalStateException(\"handler emitted non-null value\"); });","typeGuard":"static <T> Mono<Void> requireEmpty(Mono<T> mono) {\n    return mono.flatMap(v -> v == null ? Mono.empty()\n        : Mono.error(new ClassCastException(\"Expected Mono<Void> but got Mono<\" + v.getClass().getName() + \">\")));\n}","tryCatchPattern":"try {\n    callback.apply(notification).block();\n} catch (ClassCastException e) {\n    logger.error(\"Logging handler must complete empty: {}\", e.getMessage());\n}","preventionTips":["Always end logging handler chains with .then() or Mono.fromRunnable","Never use map() as the last operator in a Mono<Void> handler","Unit-test handlers by asserting the Mono completes without emitting"],"tags":["mcp","reactor","mono","return-type","java"],"backgroundTag":"unexpected-response-shape","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"}