Skip to content

Commit 6aad9cd

Browse files
authored
feat: Add tenant parameter to REST push notification config endpoints (#542)
- Add tenant parameter to RestHandler.getTaskPushNotificationConfiguration to match other push notification methods - Add route with trailing slash to distinguish GET (single config) from LIST in A2AServerRoutes - Update RestTransport to use trailing slash when configId is null - Update tests to use new method signatures - Update REST routes to match the new teannt path element This ensures consistent tenant handling across all push notification endpoints and fixes routing ambiguity between GET and LIST operations. - [X] Follow the [`CONTRIBUTING` Guide](../CONTRIBUTING.md). - [C] Make your Pull Request title in the <https://www.conventionalcommits.org/> specification. - Important Prefixes for [release-please](https://github.com/googleapis/release-please): - `fix:` which represents bug fixes, and correlates to a [SemVer](https://semver.org/) patch. - `feat:` represents a new feature, and correlates to a SemVer minor. - `feat!:`, or `fix!:`, `refactor!:`, etc., which represent a breaking change (indicated by the `!`) and will result in a SemVer major. - [X Ensure the tests pass - [X] Appropriate READMEs were updated (if necessary) Fixes #531 🦕 Signed-off-by: Emmanuel Hugonnet <[email protected]>
1 parent 7b1fa9b commit 6aad9cd

File tree

16 files changed

+213
-148
lines changed

16 files changed

+213
-148
lines changed

client/base/src/test/java/io/a2a/client/AuthenticationAuthorizationTest.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ public void testRestNonStreamingUnauthenticated() throws A2AClientException {
194194
server.when(
195195
request()
196196
.withMethod("POST")
197-
.withPath("/v1/message:send")
197+
.withPath("/message:send")
198198
).respond(
199199
response()
200200
.withStatusCode(401)
@@ -215,7 +215,7 @@ public void testRestNonStreamingUnauthorized() throws A2AClientException {
215215
server.when(
216216
request()
217217
.withMethod("POST")
218-
.withPath("/v1/message:send")
218+
.withPath("/message:send")
219219
).respond(
220220
response()
221221
.withStatusCode(403)
@@ -236,7 +236,7 @@ public void testRestStreamingUnauthenticated() throws Exception {
236236
server.when(
237237
request()
238238
.withMethod("POST")
239-
.withPath("/v1/message:stream")
239+
.withPath("/message:stream")
240240
).respond(
241241
response()
242242
.withStatusCode(401)
@@ -253,7 +253,7 @@ public void testRestStreamingUnauthorized() throws Exception {
253253
server.when(
254254
request()
255255
.withMethod("POST")
256-
.withPath("/v1/message:stream")
256+
.withPath("/message:stream")
257257
).respond(
258258
response()
259259
.withStatusCode(403)

client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransport.java

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public EventKind sendMessage(MessageSendParams messageSendParams, @Nullable Clie
8383
io.a2a.grpc.SendMessageRequest.Builder builder = io.a2a.grpc.SendMessageRequest.newBuilder(ProtoUtils.ToProto.sendMessageRequest(messageSendParams));
8484
PayloadAndHeaders payloadAndHeaders = applyInterceptors(SendMessageRequest.METHOD, builder, agentCard, context);
8585
try {
86-
String httpResponseBody = sendPostRequest(agentUrl + "/v1/message:send", payloadAndHeaders);
86+
String httpResponseBody = sendPostRequest(buildBaseUrl(messageSendParams.tenant()) + "/message:send", payloadAndHeaders);
8787
io.a2a.grpc.SendMessageResponse.Builder responseBuilder = io.a2a.grpc.SendMessageResponse.newBuilder();
8888
JsonFormat.parser().merge(httpResponseBody, responseBuilder);
8989
if (responseBuilder.hasMsg()) {
@@ -111,7 +111,7 @@ public void sendMessageStreaming(MessageSendParams messageSendParams, Consumer<S
111111
AtomicReference<CompletableFuture<Void>> ref = new AtomicReference<>();
112112
RestSSEEventListener sseEventListener = new RestSSEEventListener(eventConsumer, errorConsumer);
113113
try {
114-
A2AHttpClient.PostBuilder postBuilder = createPostBuilder(agentUrl + "/v1/message:stream", payloadAndHeaders);
114+
A2AHttpClient.PostBuilder postBuilder = createPostBuilder(buildBaseUrl(messageSendParams.tenant()) + "/message:stream", payloadAndHeaders);
115115
ref.set(postBuilder.postAsyncSSE(
116116
msg -> sseEventListener.onMessage(msg, ref.get()),
117117
throwable -> sseEventListener.onError(throwable, ref.get()),
@@ -135,13 +135,13 @@ public Task getTask(TaskQueryParams taskQueryParams, @Nullable ClientCallContext
135135
PayloadAndHeaders payloadAndHeaders = applyInterceptors(GetTaskRequest.METHOD, builder,
136136
agentCard, context);
137137
try {
138-
String url;
138+
StringBuilder url = new StringBuilder(buildBaseUrl(taskQueryParams.tenant()));
139139
if (taskQueryParams.historyLength() != null && taskQueryParams.historyLength() > 0) {
140-
url = agentUrl + String.format("/v1/tasks/%1s?historyLength=%2d", taskQueryParams.id(), taskQueryParams.historyLength());
140+
url.append(String.format("/tasks/%1s?historyLength=%2d", taskQueryParams.id(), taskQueryParams.historyLength()));
141141
} else {
142-
url = agentUrl + String.format("/v1/tasks/%1s", taskQueryParams.id());
142+
url.append(String.format("/tasks/%1s", taskQueryParams.id()));
143143
}
144-
A2AHttpClient.GetBuilder getBuilder = httpClient.createGet().url(url);
144+
A2AHttpClient.GetBuilder getBuilder = httpClient.createGet().url(url.toString());
145145
if (payloadAndHeaders.getHeaders() != null) {
146146
for (Map.Entry<String, String> entry : payloadAndHeaders.getHeaders().entrySet()) {
147147
getBuilder.addHeader(entry.getKey(), entry.getValue());
@@ -170,7 +170,7 @@ public Task cancelTask(TaskIdParams taskIdParams, @Nullable ClientCallContext co
170170
PayloadAndHeaders payloadAndHeaders = applyInterceptors(CancelTaskRequest.METHOD, builder,
171171
agentCard, context);
172172
try {
173-
String httpResponseBody = sendPostRequest(agentUrl + String.format("/v1/tasks/%1s:cancel", taskIdParams.id()), payloadAndHeaders);
173+
String httpResponseBody = sendPostRequest(buildBaseUrl(taskIdParams.tenant()) + String.format("/tasks/%1s:cancel", taskIdParams.id()), payloadAndHeaders);
174174
io.a2a.grpc.Task.Builder responseBuilder = io.a2a.grpc.Task.newBuilder();
175175
JsonFormat.parser().merge(httpResponseBody, responseBuilder);
176176
return ProtoUtils.FromProto.task(responseBuilder);
@@ -212,8 +212,8 @@ public ListTasksResult listTasks(ListTasksParams request, @Nullable ClientCallCo
212212

213213
try {
214214
// Build query string
215-
StringBuilder urlBuilder = new StringBuilder(agentUrl);
216-
urlBuilder.append("/v1/tasks");
215+
StringBuilder urlBuilder = new StringBuilder(buildBaseUrl(request.tenant()));
216+
urlBuilder.append("/tasks");
217217
String queryParams = buildListTasksQueryString(request);
218218
if (!queryParams.isEmpty()) {
219219
urlBuilder.append("?").append(queryParams);
@@ -246,6 +246,25 @@ public ListTasksResult listTasks(ListTasksParams request, @Nullable ClientCallCo
246246
}
247247
}
248248

249+
private String buildBaseUrl(@Nullable String tenant) {
250+
StringBuilder urlBuilder = new StringBuilder(agentUrl);
251+
urlBuilder.append(extractTenant(tenant));
252+
return urlBuilder.toString();
253+
}
254+
255+
private String extractTenant(@Nullable String tenant) {
256+
String tenantPath = tenant;
257+
if (tenantPath == null || tenantPath.isBlank()) {
258+
return "";
259+
}
260+
if(! tenantPath.startsWith("/")){
261+
tenantPath = '/' + tenantPath;
262+
}
263+
if(tenantPath.endsWith("/")){
264+
tenantPath = tenantPath.substring(0, tenantPath.length() -1);
265+
}
266+
return tenantPath;
267+
}
249268
private String buildListTasksQueryString(ListTasksParams request) {
250269
java.util.List<String> queryParts = new java.util.ArrayList<>();
251270
if (request.contextId() != null) {
@@ -281,7 +300,7 @@ public TaskPushNotificationConfig setTaskPushNotificationConfiguration(TaskPushN
281300
}
282301
PayloadAndHeaders payloadAndHeaders = applyInterceptors(SetTaskPushNotificationConfigRequest.METHOD, builder, agentCard, context);
283302
try {
284-
String httpResponseBody = sendPostRequest(agentUrl + String.format("/v1/tasks/%1s/pushNotificationConfigs", request.taskId()), payloadAndHeaders);
303+
String httpResponseBody = sendPostRequest(buildBaseUrl(request.tenant()) + String.format("/tasks/%1s/pushNotificationConfigs", request.taskId()), payloadAndHeaders);
285304
io.a2a.grpc.TaskPushNotificationConfig.Builder responseBuilder = io.a2a.grpc.TaskPushNotificationConfig.newBuilder();
286305
JsonFormat.parser().merge(httpResponseBody, responseBuilder);
287306
return ProtoUtils.FromProto.taskPushNotificationConfig(responseBuilder);
@@ -297,12 +316,20 @@ public TaskPushNotificationConfig getTaskPushNotificationConfiguration(GetTaskPu
297316
checkNotNullParam("request", request);
298317
io.a2a.grpc.GetTaskPushNotificationConfigRequest.Builder builder
299318
= io.a2a.grpc.GetTaskPushNotificationConfigRequest.newBuilder();
300-
builder.setName(String.format("/tasks/%1s/pushNotificationConfigs/%2s", request.id(), request.pushNotificationConfigId()));
319+
StringBuilder url = new StringBuilder(buildBaseUrl(request.tenant()));
320+
String configId = request.pushNotificationConfigId();
321+
if (configId != null && !configId.isEmpty()) {
322+
builder.setName(String.format("/tasks/%1s/pushNotificationConfigs/%2s", request.id(), configId));
323+
url.append(builder.getName());
324+
} else {
325+
// Use trailing slash to distinguish GET from LIST
326+
builder.setName(String.format("/tasks/%1s/pushNotificationConfigs/", request.id()));
327+
url.append(builder.getName());
328+
}
301329
PayloadAndHeaders payloadAndHeaders = applyInterceptors(GetTaskPushNotificationConfigRequest.METHOD, builder,
302330
agentCard, context);
303331
try {
304-
String url = agentUrl + String.format("/v1/tasks/%1s/pushNotificationConfigs/%2s", request.id(), request.pushNotificationConfigId());
305-
A2AHttpClient.GetBuilder getBuilder = httpClient.createGet().url(url);
332+
A2AHttpClient.GetBuilder getBuilder = httpClient.createGet().url(url.toString());
306333
if (payloadAndHeaders.getHeaders() != null) {
307334
for (Map.Entry<String, String> entry : payloadAndHeaders.getHeaders().entrySet()) {
308335
getBuilder.addHeader(entry.getKey(), entry.getValue());
@@ -332,7 +359,7 @@ public List<TaskPushNotificationConfig> listTaskPushNotificationConfigurations(L
332359
PayloadAndHeaders payloadAndHeaders = applyInterceptors(ListTaskPushNotificationConfigRequest.METHOD, builder,
333360
agentCard, context);
334361
try {
335-
String url = agentUrl + String.format("/v1/tasks/%1s/pushNotificationConfigs", request.id());
362+
String url = buildBaseUrl(request.tenant()) + String.format("/tasks/%1s/pushNotificationConfigs", request.id());
336363
A2AHttpClient.GetBuilder getBuilder = httpClient.createGet().url(url);
337364
if (payloadAndHeaders.getHeaders() != null) {
338365
for (Map.Entry<String, String> entry : payloadAndHeaders.getHeaders().entrySet()) {
@@ -361,7 +388,7 @@ public void deleteTaskPushNotificationConfigurations(DeleteTaskPushNotificationC
361388
PayloadAndHeaders payloadAndHeaders = applyInterceptors(DeleteTaskPushNotificationConfigRequest.METHOD, builder,
362389
agentCard, context);
363390
try {
364-
String url = agentUrl + String.format("/v1/tasks/%1s/pushNotificationConfigs/%2s", request.id(), request.pushNotificationConfigId());
391+
String url = buildBaseUrl(request.tenant()) + String.format("/tasks/%1s/pushNotificationConfigs/%2s", request.id(), request.pushNotificationConfigId());
365392
A2AHttpClient.DeleteBuilder deleteBuilder = httpClient.createDelete().url(url);
366393
if (payloadAndHeaders.getHeaders() != null) {
367394
for (Map.Entry<String, String> entry : payloadAndHeaders.getHeaders().entrySet()) {
@@ -390,7 +417,7 @@ public void resubscribe(TaskIdParams request, Consumer<StreamingEventKind> event
390417
AtomicReference<CompletableFuture<Void>> ref = new AtomicReference<>();
391418
RestSSEEventListener sseEventListener = new RestSSEEventListener(eventConsumer, errorConsumer);
392419
try {
393-
String url = agentUrl + String.format("/v1/tasks/%1s:subscribe", request.id());
420+
String url = buildBaseUrl(request.tenant()) + String.format("/tasks/%1s:subscribe", request.id());
394421
A2AHttpClient.PostBuilder postBuilder = createPostBuilder(url, payloadAndHeaders);
395422
ref.set(postBuilder.postAsyncSSE(
396423
msg -> sseEventListener.onMessage(msg, ref.get()),
@@ -421,7 +448,7 @@ public AgentCard getAgentCard(@Nullable ClientCallContext context) throws A2ACli
421448
}
422449
PayloadAndHeaders payloadAndHeaders = applyInterceptors(GetTaskRequest.METHOD, null,
423450
agentCard, context);
424-
String url = agentUrl + String.format("/v1/card");
451+
String url = buildBaseUrl("") + String.format("/extendedAgentCard");
425452
A2AHttpClient.GetBuilder getBuilder = httpClient.createGet().url(url);
426453
if (payloadAndHeaders.getHeaders() != null) {
427454
for (Map.Entry<String, String> entry : payloadAndHeaders.getHeaders().entrySet()) {

client/transport/rest/src/test/java/io/a2a/client/transport/rest/RestTransportTest.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ public void testSendMessage() throws Exception {
117117
this.server.when(
118118
request()
119119
.withMethod("POST")
120-
.withPath("/v1/message:send")
120+
.withPath("/message:send")
121121
.withBody(JsonBody.json(SEND_MESSAGE_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS))
122122
)
123123
.respond(
@@ -160,7 +160,7 @@ public void testCancelTask() throws Exception {
160160
this.server.when(
161161
request()
162162
.withMethod("POST")
163-
.withPath("/v1/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64:cancel")
163+
.withPath("/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64:cancel")
164164
.withBody(JsonBody.json(CANCEL_TASK_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS))
165165
)
166166
.respond(
@@ -186,7 +186,7 @@ public void testGetTask() throws Exception {
186186
this.server.when(
187187
request()
188188
.withMethod("GET")
189-
.withPath("/v1/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64")
189+
.withPath("/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64")
190190
)
191191
.respond(
192192
response()
@@ -238,7 +238,7 @@ public void testSendMessageStreaming() throws Exception {
238238
this.server.when(
239239
request()
240240
.withMethod("POST")
241-
.withPath("/v1/message:stream")
241+
.withPath("/message:stream")
242242
.withBody(JsonBody.json(SEND_MESSAGE_STREAMING_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS))
243243
)
244244
.respond(
@@ -290,7 +290,7 @@ public void testSetTaskPushNotificationConfiguration() throws Exception {
290290
this.server.when(
291291
request()
292292
.withMethod("POST")
293-
.withPath("/v1/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs")
293+
.withPath("/tenant/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs")
294294
.withBody(JsonBody.json(SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS))
295295
)
296296
.respond(
@@ -324,7 +324,7 @@ public void testGetTaskPushNotificationConfiguration() throws Exception {
324324
this.server.when(
325325
request()
326326
.withMethod("GET")
327-
.withPath("/v1/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs/10")
327+
.withPath("/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs/10")
328328
)
329329
.respond(
330330
response()
@@ -351,7 +351,7 @@ public void testListTaskPushNotificationConfigurations() throws Exception {
351351
this.server.when(
352352
request()
353353
.withMethod("GET")
354-
.withPath("/v1/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs")
354+
.withPath("/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs")
355355
)
356356
.respond(
357357
response()
@@ -388,7 +388,7 @@ public void testDeleteTaskPushNotificationConfigurations() throws Exception {
388388
this.server.when(
389389
request()
390390
.withMethod("DELETE")
391-
.withPath("/v1/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs/10")
391+
.withPath("/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs/10")
392392
)
393393
.respond(
394394
response()
@@ -409,7 +409,7 @@ public void testResubscribe() throws Exception {
409409
this.server.when(
410410
request()
411411
.withMethod("POST")
412-
.withPath("/v1/tasks/task-1234:subscribe")
412+
.withPath("/tasks/task-1234:subscribe")
413413
)
414414
.respond(
415415
response()

0 commit comments

Comments
 (0)