{"record":{"id":"ff339075b28ca3f4","repo":"spring-projects/spring-ai","slug":"asynchronous-progress-methods-must-return-void-or","errorCode":null,"errorMessage":"Asynchronous progress methods must return void or Mono<Void>: {method.getName()} in {method.getDeclaringClass().getName()} returns {returnType.getName()}","messagePattern":"Asynchronous progress methods must return void or 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":68,"sourceCode":"\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\n\t * method\n\t * @throws IllegalArgumentException if the notification is null\n\t */\n\t@Override\n\tpublic Mono<Void> apply(ProgressNotification notification) {\n\t\tif (notification == null) {","sourceCodeStart":50,"sourceCodeEnd":86,"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#L50-L86","documentation":"Asynchronous (@McpProgress with async callback) methods must return either void or Mono<Void>. validateReturnType throws this IllegalArgumentException as the terminal check when the declared return type is neither, e.g. a concrete type, CompletableFuture, or Flux. It enforces the async progress handler contract at registration time.","triggerScenarios":"Registering an async progress callback whose method returns boolean, CompletableFuture<Void>, Flux<Void>, ProgressNotification, or any other non-void/non-Mono<Void> type.","commonSituations":"Reusing an existing service method as a progress handler without adapting its return type; expecting async results via CompletableFuture out of habit; returning the notification object for chaining.","solutions":["Change the method's return type to void.","Or change it to Mono<Void> and make the body fully reactive, ending with .then().","If other callers need the current return type, create a dedicated @McpProgress wrapper method returning void that delegates to it."],"exampleFix":"// before\n@McpProgress\npublic CompletableFuture<Void> onProgress(Double p, String token) { ... }\n// after\n@McpProgress\npublic Mono<Void> onProgress(Double p, String token) { return CompletableFutureSupplier.then(); }","handlingStrategy":"type-guard","validationCode":"Class<?> rt = method.getReturnType();\nboolean ok = void.class.equals(rt)\n    || (Mono.class.equals(rt) && isMonoVoid(method));\nif (!ok) throw new IllegalStateException(method + \" must return void or Mono<Void>\");","typeGuard":"boolean isValidAsyncProgressReturn(Method m) {\n    Class<?> rt = m.getReturnType();\n    if (void.class.equals(rt) || Void.class.equals(rt)) return true;\n    return Mono.class.equals(rt) && isMonoVoid(m);\n}","tryCatchPattern":"try {\n    registerAsyncHandler(method, bean);\n} catch (IllegalArgumentException e) {\n    if (e.getMessage().startsWith(\"Asynchronous progress methods must return\")) {\n        throw new ConfigurationException(\"Change return type to void or Mono<Void>\", e);\n    }\n    throw e;\n}","preventionTips":["Never reuse arbitrary service methods as async progress handlers without checking the return type.","Avoid CompletableFuture/Flux in progress handlers — this API only supports void and Mono<Void>.","Document the allowed signatures next to the @McpProgress usage in your codebase."],"tags":["mcp","async","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"}