From 28482bb1979e5f2cad36d7d253048150e8ea986d Mon Sep 17 00:00:00 2001 From: sumitvairagar Date: Wed, 21 Oct 2026 10:28:46 +0530 Subject: [PATCH 1/3] fix(rest): include missing query params in listTasks and listTaskPushNotificationConfigs RestTransport.buildListTasksQueryString() iterated all fields of ListTasksParams except statusTimestampAfter, which was silently dropped from every GET /tasks request, making the server-side timestamp filter unreachable from the Java client. RestTransport.listTaskPushNotificationConfigurations() built its URL as a plain string with no query parameters at all. The pageSize and pageToken fields on ListTaskPushNotificationConfigsParams were ignored, making pagination impossible. Fix: - Add statusTimestampAfter to buildListTasksQueryString(), formatted as ISO-8601 and URL-encoded (same pattern as existing string fields). - Extract buildListPushNotificationConfigsQueryString() for the push notification configs method and append the query string to the URL. - Add regression tests for both bugs using MockServer; each test asserts the query parameter is present in the outbound request. This fixes #1233 --- .../client/transport/rest/RestTransport.java | 26 +++++++- .../transport/rest/JsonRestMessages.java | 16 +++++ .../transport/rest/RestTransportTest.java | 64 +++++++++++++++++++ 3 files changed, 103 insertions(+), 3 deletions(-) diff --git a/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/RestTransport.java b/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/RestTransport.java index 1f6535250..a02003ce6 100644 --- a/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/RestTransport.java +++ b/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/RestTransport.java @@ -241,7 +241,7 @@ public ListTasksResult listTasks(ListTasksParams request, @Nullable ClientCallCo private String buildListTasksQueryString(ListTasksParams request) { - java.util.List queryParts = new java.util.ArrayList<>(); + List queryParts = new java.util.ArrayList<>(); if (request.contextId() != null) { queryParts.add("contextId=" + URLEncoder.encode(request.contextId(), StandardCharsets.UTF_8)); } @@ -257,12 +257,27 @@ private String buildListTasksQueryString(ListTasksParams request) { if (request.historyLength() != null) { queryParts.add("historyLength=" + request.historyLength()); } + if (request.statusTimestampAfter() != null) { + queryParts.add("statusTimestampAfter=" + URLEncoder.encode( + request.statusTimestampAfter().toString(), StandardCharsets.UTF_8)); + } if (request.includeArtifacts() != null && request.includeArtifacts()) { queryParts.add("includeArtifacts=true"); } return String.join("&", queryParts); } + private String buildListPushNotificationConfigsQueryString(ListTaskPushNotificationConfigsParams request) { + List queryParts = new java.util.ArrayList<>(); + if (request.pageSize() > 0) { + queryParts.add("pageSize=" + request.pageSize()); + } + if (request.pageToken() != null && !request.pageToken().isEmpty()) { + queryParts.add("pageToken=" + URLEncoder.encode(request.pageToken(), StandardCharsets.UTF_8)); + } + return String.join("&", queryParts); + } + @Override public TaskPushNotificationConfig createTaskPushNotificationConfiguration(TaskPushNotificationConfig request, @Nullable ClientCallContext context) throws A2AClientException { checkNotNullParam("request", request); @@ -325,8 +340,13 @@ public ListTaskPushNotificationConfigsResult listTaskPushNotificationConfigurati PayloadAndHeaders payloadAndHeaders = applyInterceptors(LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, builder, agentCard, context); try { - String url = Utils.buildBaseUrl(agentInterface, request.tenant()) + String.format("/tasks/%1s/pushNotificationConfigs", request.id()); - A2AHttpClient.GetBuilder getBuilder = httpClient.createGet().url(url); + StringBuilder urlBuilder = new StringBuilder(Utils.buildBaseUrl(agentInterface, request.tenant())); + urlBuilder.append(String.format("/tasks/%1s/pushNotificationConfigs", request.id())); + String queryString = buildListPushNotificationConfigsQueryString(request); + if (!queryString.isEmpty()) { + urlBuilder.append("?").append(queryString); + } + A2AHttpClient.GetBuilder getBuilder = httpClient.createGet().url(urlBuilder.toString()); addStandardHeaders(getBuilder, payloadAndHeaders); A2AHttpResponse response = getBuilder.get(); if (!response.success()) { diff --git a/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/JsonRestMessages.java b/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/JsonRestMessages.java index 4a77b0140..0f28e0d9a 100644 --- a/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/JsonRestMessages.java +++ b/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/JsonRestMessages.java @@ -387,4 +387,20 @@ public class JsonRestMessages { "id": "task-1234" } }"""; + + static final String LIST_TASKS_TEST_RESPONSE = """ + { + "tasks": [ + { + "id": "de38c76d-d54c-436c-8b9f-4c2703648d64", + "contextId": "c295ea44-7543-4f78-b524-7a38915ad6e4", + "status": { + "state": "TASK_STATE_COMPLETED" + }, + "metadata": {} + } + ], + "totalSize": 1, + "nextPageToken": "" + }"""; } diff --git a/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/RestTransportTest.java b/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/RestTransportTest.java index 05d1b7b12..2c1cab1db 100644 --- a/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/RestTransportTest.java +++ b/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/RestTransportTest.java @@ -6,6 +6,7 @@ import static org.a2aproject.sdk.client.transport.rest.JsonRestMessages.GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE; import static org.a2aproject.sdk.client.transport.rest.JsonRestMessages.GET_TASK_TEST_RESPONSE; import static org.a2aproject.sdk.client.transport.rest.JsonRestMessages.LIST_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE; +import static org.a2aproject.sdk.client.transport.rest.JsonRestMessages.LIST_TASKS_TEST_RESPONSE; import static org.a2aproject.sdk.client.transport.rest.JsonRestMessages.SEND_MESSAGE_STREAMING_TEST_REQUEST; import static org.a2aproject.sdk.client.transport.rest.JsonRestMessages.SEND_MESSAGE_STREAMING_TEST_RESPONSE; import static org.a2aproject.sdk.client.transport.rest.JsonRestMessages.SEND_MESSAGE_TEST_REQUEST; @@ -22,6 +23,7 @@ import static org.mockserver.model.HttpResponse.response; import java.io.IOException; +import java.time.Instant; import java.util.Collections; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -48,6 +50,7 @@ import org.a2aproject.sdk.spec.GetTaskPushNotificationConfigParams; import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsParams; import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsResult; +import org.a2aproject.sdk.spec.ListTasksParams; import org.a2aproject.sdk.spec.Message; import org.a2aproject.sdk.spec.MessageSendConfiguration; import org.a2aproject.sdk.spec.MessageSendParams; @@ -633,4 +636,65 @@ public void testVersionNotSupportedErrorUnmarshallingStreaming() throws Exceptio assertTrue(versionError.getMessage().contains("2.0")); assertTrue(versionError.getMessage().contains("1.0")); } + + /** + * Regression test for #1233: listTasks must include statusTimestampAfter in the query string. + * The mock server only responds when the parameter is present; without the fix the mock will + * not match and the client throws an exception, causing the assertion to fail. + */ + @Test + public void testListTasksIncludesStatusTimestampAfterQueryParam() throws Exception { + Instant cutoff = Instant.parse("2026-10-01T00:00:00Z"); + this.server.when( + request() + .withMethod("GET") + .withPath("/tasks") + .withQueryStringParameter("statusTimestampAfter", "2026-10-01T00:00:00Z") + ) + .respond( + response() + .withStatusCode(200) + .withBody(LIST_TASKS_TEST_RESPONSE) + ); + + RestTransport client = new RestTransport(CARD); + ListTasksParams params = ListTasksParams.builder() + .statusTimestampAfter(cutoff) + .build(); + + var result = client.listTasks(params, null); + assertEquals(1, result.tasks().size()); + assertEquals("de38c76d-d54c-436c-8b9f-4c2703648d64", result.tasks().get(0).id()); + } + + /** + * Regression test for #1233: listTaskPushNotificationConfigurations must include pageSize and + * pageToken in the query string. The mock server only responds when both parameters are present; + * without the fix the mock will not match and the client throws an exception. + */ + @Test + public void testListTaskPushNotificationConfigurationsIncludesPaginationQueryParams() throws Exception { + this.server.when( + request() + .withMethod("GET") + .withPath("/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs") + .withQueryStringParameter("pageSize", "10") + .withQueryStringParameter("pageToken", "next-page-token") + ) + .respond( + response() + .withStatusCode(200) + .withBody(LIST_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE) + ); + + RestTransport client = new RestTransport(CARD); + ListTaskPushNotificationConfigsParams params = ListTaskPushNotificationConfigsParams.builder() + .id("de38c76d-d54c-436c-8b9f-4c2703648d64") + .pageSize(10) + .pageToken("next-page-token") + .build(); + + ListTaskPushNotificationConfigsResult result = client.listTaskPushNotificationConfigurations(params, null); + assertEquals(2, result.configs().size()); + } } From e95a10a000c5916ba41bf1e3f1aa9c1873a1ffb3 Mon Sep 17 00:00:00 2001 From: sumitvairagar Date: Wed, 21 Oct 2026 19:54:36 +0530 Subject: [PATCH 2/3] fix(rest): set missing proto builder fields and URL-encode status param Three issues identified in review by @ehsavoie: 1. listTasks proto builder did not set statusTimestampAfter. Interceptors (auth, tracing) receive the builder before the REST URL is built, so missing fields there are invisible to the interceptor chain. Added the Timestamp conversion using getEpochSecond()/getNano(). 2. listTaskPushNotificationConfigurations proto builder set only taskId. pageSize and pageToken were never set, making pagination invisible to interceptors. Added the two missing fields. 3. status enum value in buildListTasksQueryString was appended raw without URL-encoding. Applied URLEncoder.encode() for consistency with all other string parameters in the same method. Added two regression tests using a capturing ClientCallInterceptor to assert the proto builder carries the correct fields before the HTTP request is sent. This fixes #1233 --- .../client/transport/rest/RestTransport.java | 14 +++- .../transport/rest/RestTransportTest.java | 80 +++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/RestTransport.java b/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/RestTransport.java index a02003ce6..068f7e700 100644 --- a/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/RestTransport.java +++ b/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/RestTransport.java @@ -204,6 +204,12 @@ public ListTasksResult listTasks(ListTasksParams request, @Nullable ClientCallCo if (request.includeArtifacts() != null && request.includeArtifacts()) { builder.setIncludeArtifacts(true); } + if (request.statusTimestampAfter() != null) { + builder.setStatusTimestampAfter(com.google.protobuf.Timestamp.newBuilder() + .setSeconds(request.statusTimestampAfter().getEpochSecond()) + .setNanos(request.statusTimestampAfter().getNano()) + .build()); + } PayloadAndHeaders payloadAndHeaders = applyInterceptors(LIST_TASK_METHOD, builder, agentCard, context); @@ -246,7 +252,7 @@ private String buildListTasksQueryString(ListTasksParams request) { queryParts.add("contextId=" + URLEncoder.encode(request.contextId(), StandardCharsets.UTF_8)); } if (request.status() != null) { - queryParts.add("status=" + request.status()); + queryParts.add("status=" + URLEncoder.encode(request.status().toString(), StandardCharsets.UTF_8)); } if (request.pageSize() != null) { queryParts.add("pageSize=" + request.pageSize()); @@ -337,6 +343,12 @@ public ListTaskPushNotificationConfigsResult listTaskPushNotificationConfigurati org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest.Builder builder = org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest.newBuilder(); builder.setTaskId(request.id()); + if (request.pageSize() > 0) { + builder.setPageSize(request.pageSize()); + } + if (request.pageToken() != null && !request.pageToken().isEmpty()) { + builder.setPageToken(request.pageToken()); + } PayloadAndHeaders payloadAndHeaders = applyInterceptors(LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, builder, agentCard, context); try { diff --git a/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/RestTransportTest.java b/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/RestTransportTest.java index 2c1cab1db..28e46d583 100644 --- a/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/RestTransportTest.java +++ b/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/RestTransportTest.java @@ -33,6 +33,9 @@ import java.util.logging.Logger; import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallContext; +import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallInterceptor; +import org.a2aproject.sdk.client.transport.spi.interceptors.PayloadAndHeaders; +import org.a2aproject.sdk.spec.AgentInterface; import org.a2aproject.sdk.spec.A2AClientException; import org.a2aproject.sdk.spec.AgentCapabilities; import org.a2aproject.sdk.spec.AgentCard; @@ -637,6 +640,83 @@ public void testVersionNotSupportedErrorUnmarshallingStreaming() throws Exceptio assertTrue(versionError.getMessage().contains("1.0")); } + /** + * Regression test for #1233: listTasks proto builder must set statusTimestampAfter so + * interceptors (auth, tracing, etc.) see the complete request. + */ + @Test + public void testListTasksProtoBuilderIncludesStatusTimestampAfter() throws Exception { + Instant cutoff = Instant.parse("2026-10-01T00:00:00Z"); + AtomicReference capturedPayload = new AtomicReference<>(); + + ClientCallInterceptor capturingInterceptor = new ClientCallInterceptor() { + @Override + public PayloadAndHeaders intercept(String methodName, Object payload, + java.util.Map headers, org.a2aproject.sdk.spec.AgentCard agentCard, + ClientCallContext clientCallContext) { + capturedPayload.set(payload); + return new PayloadAndHeaders(payload, headers); + } + }; + + this.server.when( + request().withMethod("GET").withPath("/tasks") + ).respond(response().withStatusCode(200).withBody(LIST_TASKS_TEST_RESPONSE)); + + AgentInterface agentInterface = new AgentInterface("HTTP+JSON", "http://localhost:4001"); + RestTransport client = new RestTransport(null, CARD, agentInterface, List.of(capturingInterceptor)); + + ListTasksParams params = ListTasksParams.builder().statusTimestampAfter(cutoff).build(); + client.listTasks(params, null); + + assertNotNull(capturedPayload.get()); + org.a2aproject.sdk.grpc.ListTasksRequest proto = + ((org.a2aproject.sdk.grpc.ListTasksRequest.Builder) capturedPayload.get()).build(); + assertTrue(proto.hasStatusTimestampAfter(), "proto builder must set statusTimestampAfter"); + assertEquals(cutoff.getEpochSecond(), proto.getStatusTimestampAfter().getSeconds()); + assertEquals(cutoff.getNano(), proto.getStatusTimestampAfter().getNanos()); + } + + /** + * Regression test for #1233: listTaskPushNotificationConfigurations proto builder must set + * pageSize and pageToken so interceptors see the complete request. + */ + @Test + public void testListTaskPushNotificationConfigurationsProtoBuilderIncludesPagination() throws Exception { + AtomicReference capturedPayload = new AtomicReference<>(); + + ClientCallInterceptor capturingInterceptor = new ClientCallInterceptor() { + @Override + public PayloadAndHeaders intercept(String methodName, Object payload, + java.util.Map headers, org.a2aproject.sdk.spec.AgentCard agentCard, + ClientCallContext clientCallContext) { + capturedPayload.set(payload); + return new PayloadAndHeaders(payload, headers); + } + }; + + this.server.when( + request().withMethod("GET") + .withPath("/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs") + ).respond(response().withStatusCode(200).withBody(LIST_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE)); + + AgentInterface agentInterface = new AgentInterface("HTTP+JSON", "http://localhost:4001"); + RestTransport client = new RestTransport(null, CARD, agentInterface, List.of(capturingInterceptor)); + + ListTaskPushNotificationConfigsParams params = ListTaskPushNotificationConfigsParams.builder() + .id("de38c76d-d54c-436c-8b9f-4c2703648d64") + .pageSize(10) + .pageToken("next-page-token") + .build(); + client.listTaskPushNotificationConfigurations(params, null); + + assertNotNull(capturedPayload.get()); + org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest proto = + ((org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest.Builder) capturedPayload.get()).build(); + assertEquals(10, proto.getPageSize(), "proto builder must set pageSize"); + assertEquals("next-page-token", proto.getPageToken(), "proto builder must set pageToken"); + } + /** * Regression test for #1233: listTasks must include statusTimestampAfter in the query string. * The mock server only responds when the parameter is present; without the fix the mock will From 2eac4bad862abd9af8aaf0a402fcd91a9f64acd9 Mon Sep 17 00:00:00 2001 From: sumitvairagar Date: Wed, 21 Oct 2026 20:06:40 +0530 Subject: [PATCH 3/3] style: match original file conventions in RestTransport and RestTransportTest - Restore java.util.List/java.util.ArrayList<> FQN style in buildListTasksQueryString and buildListPushNotificationConfigsQueryString to match the pre-existing pattern in the original file - Replace org.a2aproject.sdk.spec.AgentCard FQN with short name in test interceptors (AgentCard is already imported) - Fix AgentInterface import to correct alphabetical position in test file This fixes #1233 --- .../a2aproject/sdk/client/transport/rest/RestTransport.java | 4 ++-- .../sdk/client/transport/rest/RestTransportTest.java | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/RestTransport.java b/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/RestTransport.java index 068f7e700..f060a8814 100644 --- a/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/RestTransport.java +++ b/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/RestTransport.java @@ -247,7 +247,7 @@ public ListTasksResult listTasks(ListTasksParams request, @Nullable ClientCallCo private String buildListTasksQueryString(ListTasksParams request) { - List queryParts = new java.util.ArrayList<>(); + java.util.List queryParts = new java.util.ArrayList<>(); if (request.contextId() != null) { queryParts.add("contextId=" + URLEncoder.encode(request.contextId(), StandardCharsets.UTF_8)); } @@ -274,7 +274,7 @@ private String buildListTasksQueryString(ListTasksParams request) { } private String buildListPushNotificationConfigsQueryString(ListTaskPushNotificationConfigsParams request) { - List queryParts = new java.util.ArrayList<>(); + java.util.List queryParts = new java.util.ArrayList<>(); if (request.pageSize() > 0) { queryParts.add("pageSize=" + request.pageSize()); } diff --git a/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/RestTransportTest.java b/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/RestTransportTest.java index 28e46d583..fd2534f45 100644 --- a/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/RestTransportTest.java +++ b/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/RestTransportTest.java @@ -35,10 +35,10 @@ import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallContext; import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallInterceptor; import org.a2aproject.sdk.client.transport.spi.interceptors.PayloadAndHeaders; -import org.a2aproject.sdk.spec.AgentInterface; import org.a2aproject.sdk.spec.A2AClientException; import org.a2aproject.sdk.spec.AgentCapabilities; import org.a2aproject.sdk.spec.AgentCard; +import org.a2aproject.sdk.spec.AgentInterface; import org.a2aproject.sdk.spec.AgentSkill; import org.a2aproject.sdk.spec.Artifact; import org.a2aproject.sdk.spec.AuthenticationInfo; @@ -652,7 +652,7 @@ public void testListTasksProtoBuilderIncludesStatusTimestampAfter() throws Excep ClientCallInterceptor capturingInterceptor = new ClientCallInterceptor() { @Override public PayloadAndHeaders intercept(String methodName, Object payload, - java.util.Map headers, org.a2aproject.sdk.spec.AgentCard agentCard, + java.util.Map headers, AgentCard agentCard, ClientCallContext clientCallContext) { capturedPayload.set(payload); return new PayloadAndHeaders(payload, headers); @@ -688,7 +688,7 @@ public void testListTaskPushNotificationConfigurationsProtoBuilderIncludesPagina ClientCallInterceptor capturingInterceptor = new ClientCallInterceptor() { @Override public PayloadAndHeaders intercept(String methodName, Object payload, - java.util.Map headers, org.a2aproject.sdk.spec.AgentCard agentCard, + java.util.Map headers, AgentCard agentCard, ClientCallContext clientCallContext) { capturedPayload.set(payload); return new PayloadAndHeaders(payload, headers);