This is an automated email from the ASF dual-hosted git repository.
Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git
The following commit(s) were added to refs/heads/master by this push:
new 9bd13166e5 fix(agent-gateway): validate initialized notification
acknowledgements (#7805)
9bd13166e5 is described below
commit 9bd13166e567da4dc54dc706bfface4bc4a95479
Author: BobSong <[email protected]>
AuthorDate: Mon Oct 19 14:01:19 2026 +0800
fix(agent-gateway): validate initialized notification acknowledgements
(#7805)
Co-authored-by: BobSong-dev <[email protected]>
---
.../gateway/remote/RequestScopedMcpClient.java | 3 ++
.../gateway/remote/RequestScopedMcpClientTest.java | 62 +++++++++++++++++++++-
2 files changed, 64 insertions(+), 1 deletion(-)
diff --git
a/shenyu-plugin/shenyu-plugin-agent-gateway/src/main/java/org/apache/shenyu/plugin/agent/gateway/remote/RequestScopedMcpClient.java
b/shenyu-plugin/shenyu-plugin-agent-gateway/src/main/java/org/apache/shenyu/plugin/agent/gateway/remote/RequestScopedMcpClient.java
index 1d5a75001d..a635c22ea6 100644
---
a/shenyu-plugin/shenyu-plugin-agent-gateway/src/main/java/org/apache/shenyu/plugin/agent/gateway/remote/RequestScopedMcpClient.java
+++
b/shenyu-plugin/shenyu-plugin-agent-gateway/src/main/java/org/apache/shenyu/plugin/agent/gateway/remote/RequestScopedMcpClient.java
@@ -247,6 +247,9 @@ public final class RequestScopedMcpClient implements
RemoteMcpEndpoint {
session =
RemoteTransportPolicy.session(response.headers().allValues("MCP-Session-Id"));
}
if (notification) {
+ if (response.statusCode() != 202 || response.body().length != 0) {
+ throw new IllegalArgumentException("Invalid notification
acknowledgement");
+ }
return JSON.createObjectNode();
}
String contentType =
response.headers().firstValue("Content-Type").orElse("").split(";")[0].trim();
diff --git
a/shenyu-plugin/shenyu-plugin-agent-gateway/src/test/java/org/apache/shenyu/plugin/agent/gateway/remote/RequestScopedMcpClientTest.java
b/shenyu-plugin/shenyu-plugin-agent-gateway/src/test/java/org/apache/shenyu/plugin/agent/gateway/remote/RequestScopedMcpClientTest.java
index e76052a363..f9b47abf52 100644
---
a/shenyu-plugin/shenyu-plugin-agent-gateway/src/test/java/org/apache/shenyu/plugin/agent/gateway/remote/RequestScopedMcpClientTest.java
+++
b/shenyu-plugin/shenyu-plugin-agent-gateway/src/test/java/org/apache/shenyu/plugin/agent/gateway/remote/RequestScopedMcpClientTest.java
@@ -82,6 +82,59 @@ class RequestScopedMcpClientTest {
private static final Duration WAIT = Duration.ofSeconds(5);
+ @ParameterizedTest
+ @ValueSource(ints = {200, 201, 204, 206})
+ void rejectsInvalidInitializedStatus(final int status) throws Exception {
+ try (Fixture fixture = new Fixture(false)) {
+ fixture.initializedStatus = status;
+ assertRejectedAcknowledgement(fixture);
+ }
+ }
+
+ @Test
+ void rejectsNonEmptyInitializedAcknowledgement() throws Exception {
+ try (Fixture fixture = new Fixture(false)) {
+ fixture.initializedBody = "{}".getBytes(StandardCharsets.UTF_8);
+ assertRejectedAcknowledgement(fixture);
+ }
+ }
+
+ @Test
+ void acceptsEmptyInitializedAcknowledgement() throws Exception {
+ try (Fixture fixture = new Fixture(false)) {
+ RequestScopedMcpClient client = fixture.client();
+ try {
+ assertEquals("2025-06-18",
client.initialize().block(WAIT).protocolVersion());
+ assertEquals(1,
client.listTools(null).block(WAIT).tools().size());
+ awaitIdle(client);
+ assertEquals(1, fixture.requests.stream().filter(request ->
"initialize".equals(request.get("method"))).count());
+ } finally {
+ client.closeGracefully().block(WAIT);
+ }
+ assertEquals(1, fixture.deletes);
+ }
+ }
+
+ private void assertRejectedAcknowledgement(final Fixture fixture) throws
InterruptedException {
+ RequestScopedMcpClient client = fixture.client();
+ try {
+ for (int attempt = 0; attempt < 2; attempt++) {
+
StepVerifier.create(client.initialize()).expectErrorMatches(error -> error
instanceof IllegalArgumentException
+ && "Invalid notification
acknowledgement".equals(error.getMessage())).verify(WAIT);
+ }
+
StepVerifier.create(client.listTools(null)).expectError(IllegalStateException.class).verify(WAIT);
+ StepVerifier.create(client.callRaw(new
McpSchema.CallToolRequest("lookup", Map.of())))
+ .expectError(IllegalStateException.class).verify(WAIT);
+ awaitIdle(client);
+ assertEquals(2, fixture.requests.size());
+ assertEquals(1, fixture.requests.stream().filter(request ->
"initialize".equals(request.get("method"))).count());
+ assertEquals(1, fixture.requests.stream().filter(request ->
"notifications/initialized".equals(request.get("method"))).count());
+ } finally {
+ client.closeGracefully().block(WAIT);
+ }
+ assertEquals(1, fixture.deletes);
+ }
+
@ParameterizedTest
@ValueSource(booleans = {false, true})
void expiredDeadlineIsATimeoutWithoutAnUpstreamRequest(final boolean sse)
throws Exception {
@@ -381,6 +434,10 @@ class RequestScopedMcpClientTest {
private volatile int deleteStatus = 204;
+ private volatile int initializedStatus = 202;
+
+ private volatile byte[] initializedBody = new byte[0];
+
private volatile int deletes;
private Fixture(final boolean sse) throws Exception {
@@ -405,7 +462,10 @@ class RequestScopedMcpClientTest {
"cookie",
Objects.toString(exchange.getRequestHeaders().getFirst("Cookie"), "")));
if ("notifications/initialized".equals(method)) {
responseBytes.put(request.path("id").asText(), 0);
- exchange.sendResponseHeaders(202, -1);
+ exchange.sendResponseHeaders(initializedStatus,
initializedBody.length == 0 ? -1 : initializedBody.length);
+ if (initializedBody.length > 0) {
+ exchange.getResponseBody().write(initializedBody);
+ }
return;
}
ObjectNode result = json.createObjectNode();