{"record":{"id":"eb68bbd34cb18d13","repo":"spring-projects/spring-ai","slug":"unsupported-exchange-type-exchangeclassname-null","errorCode":null,"errorMessage":"Unsupported exchange type: {exchangeClassName|null} for Sync method: {methodName} in {className}","messagePattern":"Unsupported exchange type: (.+?) for Sync method: (.+?) in (.+?)","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/resource/SyncMcpResourceMethodCallback.java","lineNumber":88,"sourceCode":"\t\tif (McpTransportContext.class.isAssignableFrom(paramType)) {\n\t\t\tif (exchange instanceof McpTransportContext transportContext) {\n\t\t\t\treturn transportContext;\n\t\t\t}\n\t\t\telse if (exchange instanceof McpSyncServerExchange syncServerExchange) {\n\t\t\t\treturn syncServerExchange.transportContext();\n\t\t\t}\n\t\t\telse if (exchange instanceof McpAsyncServerExchange asyncServerExchange) {\n\t\t\t\tthrow new IllegalArgumentException(\"Unsupported Async exchange type: \"\n\t\t\t\t\t\t+ asyncServerExchange.getClass().getName() + \" for Sync method: \" + method.getName() + \" in \"\n\t\t\t\t\t\t+ method.getDeclaringClass().getName());\n\t\t\t}\n\t\t}\n\t\telse if (McpSyncServerExchange.class.isAssignableFrom(paramType)) {\n\t\t\tif (exchange instanceof McpSyncServerExchange syncServerExchange) {\n\t\t\t\treturn syncServerExchange;\n\t\t\t}\n\n\t\t\tthrow new IllegalArgumentException(\n\t\t\t\t\t\"Unsupported exchange type: \" + (exchange != null ? exchange.getClass().getName() : \"null\")\n\t\t\t\t\t\t\t+ \" for Sync method: \" + method.getName() + \" in \" + method.getDeclaringClass().getName());\n\t\t}\n\n\t\tthrow new IllegalArgumentException(\n\t\t\t\t\"Unsupported exchange type: \" + (exchange != null ? exchange.getClass().getName() : \"null\")\n\t\t\t\t\t\t+ \" for method: \" + method.getName() + \" in \" + method.getDeclaringClass().getName());\n\t}\n\n\t/**\n\t * Apply the callback to the given exchange and request.\n\t * <p>\n\t * This method extracts URI variable values from the request URI, builds the arguments\n\t * for the method call, invokes the method, and converts the result to a\n\t * ReadResourceResult.\n\t * @param exchange The server exchange, may be null if the method doesn't require it\n\t * @param request The resource request, must not be null\n\t * @return The resource result","sourceCodeStart":70,"sourceCodeEnd":106,"githubUrl":"https://github.com/spring-projects/spring-ai/blob/98a7beda4f29d80a71c5837eb4053b03a93a46f7/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/resource/SyncMcpResourceMethodCallback.java#L70-L106","documentation":"When the sync resource method declares a McpSyncServerExchange parameter, assignExchangeType requires the runtime exchange argument to be exactly an McpSyncServerExchange. If it is absent (null) or any other type (async exchange, transport context, arbitrary object), the library cannot supply the sync exchange and throws IllegalArgumentException.","triggerScenarios":"Invoking the sync resource method callback with exchange=null while the method signature demands McpSyncServerExchange, or with an McpAsyncServerExchange/McpTransportContext instead of a sync exchange.","commonSituations":"Calling the callback directly from tests with the wrong exchange object, or a framework wiring bug where the async dispatcher routes a request to a sync-registered handler.","solutions":["Pass a non-null McpSyncServerExchange when applying the callback (e.g., in tests, build one from the sync server session).","Register the handler with the correct (sync) server so the framework supplies the matching exchange.","If the method does not need an exchange, drop the McpSyncServerExchange parameter from the signature."],"exampleFix":"// before (test)\nReadResourceResult r = callback.apply(null, new ReadResourceRequest(new McpResourceUri(\"res://x\")));\n\n// after\nMcpSyncServerExchange exchange = ...; // from sync server session\nReadResourceResult r = callback.apply(exchange, new ReadResourceRequest(new McpResourceUri(\"res://x\")));","handlingStrategy":"validation","validationCode":"void requireSyncExchange(Object exchange) {\n    if (!(exchange instanceof McpSyncServerExchange))\n        throw new IllegalArgumentException(\"Expected McpSyncServerExchange, got: \" + exchange);\n}","typeGuard":"static boolean hasSyncExchange(Object exchange) {\n    return exchange instanceof McpSyncServerExchange;\n}","tryCatchPattern":"try {\n    return callback.apply(exchange, request);\n} catch (IllegalArgumentException e) {\n    if (e.getMessage().startsWith(\"Unsupported exchange type\")) {\n        log.error(\"Resource method requires McpSyncServerExchange; supplied exchange was wrong or null\", e);\n    } throw e;\n}","preventionTips":["In tests, obtain a real McpSyncServerExchange from the sync server session instead of passing null.","Do not remove exchange parameters when a method does not need them — prefer omitting them from the signature.","Cover every resource method with a through-the-server integration test."],"tags":["mcp","exchange-type","null-argument","illegal-argument"],"backgroundTag":"type-mismatch","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"}