{"record":{"id":"6618127399eb451a","repo":"spring-projects/spring-ai","slug":"synchronous-progress-methods-must-return-void-me","errorCode":null,"errorMessage":"Synchronous progress methods must return void: {method.getName()} in {method.getDeclaringClass().getName()} returns {method.getReturnType().getName()}","messagePattern":"Synchronous progress methods must return 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/SyncMcpProgressMethodCallback.java","lineNumber":43,"sourceCode":" * Synchronous implementation of a progress method callback.\n *\n * This class creates a Consumer that invokes a method annotated with @McpProgress\n * synchronously when a progress notification is received.\n *\n * @author Christian Tzolov\n */\npublic final class SyncMcpProgressMethodCallback extends AbstractMcpProgressMethodCallback\n\t\timplements Consumer<ProgressNotification> {\n\n\tprivate SyncMcpProgressMethodCallback(Builder builder) {\n\t\tsuper(builder.method, builder.bean);\n\t}\n\n\t@Override\n\tprotected void validateReturnType(Method method) {\n\t\t// Synchronous methods must return void\n\t\tif (!void.class.equals(method.getReturnType())) {\n\t\t\tthrow new IllegalArgumentException(\"Synchronous progress methods must return void: \" + method.getName()\n\t\t\t\t\t+ \" in \" + method.getDeclaringClass().getName() + \" returns \" + method.getReturnType().getName());\n\t\t}\n\t}\n\n\t/**\n\t * Accept the progress notification and process it.\n\t * <p>\n\t * This method builds the arguments for the method call and invokes the method.\n\t * @param notification The progress notification, must not be null\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 void accept(ProgressNotification notification) {\n\t\tif (notification == null) {\n\t\t\tthrow new IllegalArgumentException(\"Notification must not be null\");\n\t\t}","sourceCodeStart":25,"sourceCodeEnd":61,"githubUrl":"https://github.com/spring-projects/spring-ai/blob/98a7beda4f29d80a71c5837eb4053b03a93a46f7/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/progress/SyncMcpProgressMethodCallback.java#L25-L61","documentation":"Synchronous (@McpProgress with SyncMcpProgressMethodCallback) progress methods must return void. validateReturnType throws this IllegalArgumentException when the method declares any other return type, because the sync callback processes the notification inline and discards results.","triggerScenarios":"Registering SyncMcpProgressMethodCallback with a method that returns boolean, Double, String, or Mono — anything other than void/primitive void — e.g. a handler written as `public boolean onProgress(Double p, String token)`.","commonSituations":"Using the sync callback with a handler originally written for the async API (returns Mono<Void>); handlers that return a status flag out of habit; refactoring that changed void to a return type for testability.","solutions":["Change the method's return type to void.","If a reactive implementation is needed, register with AsyncMcpProgressMethodCallback and return Mono<Void> instead.","Expose testability via side effects (record state to a field/mock) rather than a return value."],"exampleFix":"// before\n@McpProgress\npublic boolean onProgress(Double p, String token) { return true; }\n// after\n@McpProgress\npublic void onProgress(Double p, String token) { /* handle */ }","handlingStrategy":"type-guard","validationCode":"if (!void.class.equals(method.getReturnType())) {\n    throw new IllegalStateException(method + \" must return void for sync progress callback\");\n}","typeGuard":"boolean isValidSyncProgressReturn(Method m) {\n    return void.class.equals(m.getReturnType());\n}","tryCatchPattern":"try {\n    syncRegistry.register(SyncMcpProgressMethodCallback.builder().method(m).bean(b).build());\n} catch (IllegalArgumentException e) {\n    if (e.getMessage().startsWith(\"Synchronous progress methods must return void\")) {\n        throw new ConfigurationException(\"Use void (sync) or Mono<Void> with the async callback\", e);\n    }\n    throw e;\n}","preventionTips":["Pick the sync vs async callback based on the handler's return type, not the other way around.","Keep @McpProgress handlers void for simple synchronous handling.","Add a startup test that builds all sync callbacks to catch signature drift."],"tags":["mcp","sync","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-14T11:17:12.474Z"}