{"record":{"id":"954f76d39a98224e","repo":"spring-projects/spring-ai","slug":"mono-return-type-must-be-mono-void-method-getna","errorCode":null,"errorMessage":"Mono return type must be Mono<Void>: {method.getName()} in {method.getDeclaringClass().getName()} returns {returnType.getName()}","messagePattern":"Mono return type must be Mono<Void>: (.+?) in (.+?) returns (.+?)","errorType":"validation","errorClass":"java.lang.IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/progress/AsyncMcpProgressMethodCallback.java","lineNumber":62,"sourceCode":"\t\tClass<?> returnType = method.getReturnType();\n\n\t\t// Check if return type is void or Mono<Void>\n\t\tif (returnType == void.class) {\n\t\t\t// void is acceptable - we'll wrap it in Mono\n\t\t\treturn;\n\t\t}\n\n\t\tif (Mono.class.isAssignableFrom(returnType)) {\n\t\t\t// Check if it's Mono<Void>\n\t\t\tType genericReturnType = method.getGenericReturnType();\n\t\t\tif (genericReturnType instanceof ParameterizedType paramType) {\n\t\t\t\tType[] typeArguments = paramType.getActualTypeArguments();\n\t\t\t\tif (typeArguments.length == 1 && typeArguments[0] == Void.class) {\n\t\t\t\t\t// Mono<Void> is acceptable\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tthrow new IllegalArgumentException(\"Mono return type must be Mono<Void>: \" + method.getName()\n\t\t\t\t\t\t\t+ \" in \" + method.getDeclaringClass().getName() + \" returns \" + returnType.getName());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthrow new IllegalArgumentException(\n\t\t\t\t\"Asynchronous progress methods must return void or Mono<Void>: \" + method.getName() + \" in \"\n\t\t\t\t\t\t+ method.getDeclaringClass().getName() + \" returns \" + returnType.getName());\n\t}\n\n\t/**\n\t * Apply the progress notification and process it asynchronously.\n\t * <p>\n\t * This method builds the arguments for the method call and invokes the method,\n\t * returning a Mono<Void>.\n\t * @param notification The progress notification, must not be null\n\t * @return A Mono<Void> representing the asynchronous operation\n\t * @throws McpProgressMethodException if there is an error invoking the progress","sourceCodeStart":44,"sourceCodeEnd":80,"githubUrl":"https://github.com/spring-projects/spring-ai/blob/98a7beda4f29d80a71c5837eb4053b03a93a46f7/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/progress/AsyncMcpProgressMethodCallback.java#L44-L80","documentation":"Async progress methods may return void or Mono<Void>. When a Mono is detected, validateReturnType additionally requires its generic type argument to be exactly Void. A Mono of any other type (Mono<String>, Mono<Boolean>, raw Mono) is rejected with this IllegalArgumentException so reactive progress handlers never emit an unexpected value downstream.","triggerScenarios":"Annotating a method that returns Mono<String>, Mono<Boolean>, or a raw/parameterized Mono with a non-Void type argument, then registering it with AsyncMcpProgressMethodCallback; validateReturnType runs at registration.","commonSituations":"Returning the result of a reactive pipeline (e.g. Mono.just(\"done\")) instead of terminating with .then() or Mono.empty(); raw Mono from legacy Java 6-style code without generics.","solutions":["Change the return type to Mono<Void> and end the reactive chain with .then().","Or return plain void and perform the work synchronously inside the method.","Replace Mono<SomeType> with Mono<Void> and consume the value inside the chain before then()."],"exampleFix":"// before\n@McpProgress\npublic Mono<String> onProgress(Double p, String token) { return Mono.just(\"ok\"); }\n// after\n@McpProgress\npublic Mono<Void> onProgress(Double p, String token) { return doWork(p, token).then(); }","handlingStrategy":"type-guard","validationCode":"if (Mono.class.equals(method.getReturnType())) {\n    Type arg = ((ParameterizedType) method.getGenericReturnType()).getActualTypeArguments()[0];\n    if (arg != Void.class) throw new IllegalStateException(method + \" must return Mono<Void>\");\n}","typeGuard":"boolean isMonoVoid(Method m) {\n    if (!Mono.class.equals(m.getReturnType())) return false;\n    if (!(m.getGenericReturnType() instanceof ParameterizedType pt)) return false;\n    return pt.getActualTypeArguments().length == 1 && pt.getActualTypeArguments()[0] == Void.class;\n}","tryCatchPattern":"try {\n    asyncRegistry.register(builder.method(m).bean(b).build());\n} catch (IllegalArgumentException e) {\n    if (e.getMessage().startsWith(\"Mono return type must be Mono<Void>\")) {\n        throw new ConfigurationException(\"End reactive chains with .then() to get Mono<Void>\", e);\n    }\n    throw e;\n}","preventionTips":["Standardize on ending progress handlers with .then() or Mono.empty().","Avoid raw Mono return types.","Assert return types of @McpProgress methods in a unit test."],"tags":["mcp","reactive","return-type","progress-notification"],"backgroundTag":"invalid-return-type","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"}