Skip to content

Commit 14fece4

Browse files
authored
fix: Setting the proper content type for errors. (a2aproject#720)
The expected content type is application/problem+json when there is an error and not application/json Fixes a2aproject#719 🦕 Signed-off-by: Emmanuel Hugonnet <ehugonne@redhat.com>
1 parent 20b6555 commit 14fece4

5 files changed

Lines changed: 78 additions & 25 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package io.a2a.common;
2+
3+
public interface MediaType {
4+
5+
public static final String APPLICATION_JSON = "application/json";
6+
public static final String APPLICATION_PROBLEM_JSON = "application/problem+json";
7+
}

reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.a2a.server.apps.quarkus;
22

3+
import static io.a2a.common.MediaType.APPLICATION_PROBLEM_JSON;
34
import static io.a2a.server.ServerCallContext.TRANSPORT_KEY;
45
import static io.a2a.transport.jsonrpc.context.JSONRPCContextKeys.HEADERS_KEY;
56
import static io.a2a.transport.jsonrpc.context.JSONRPCContextKeys.METHOD_NAME_KEY;
@@ -293,7 +294,7 @@ public void invokeJSONRPCHandler(@Body String body, RoutingContext rc) {
293294
if (error != null) {
294295
rc.response()
295296
.setStatusCode(200)
296-
.putHeader(CONTENT_TYPE, APPLICATION_JSON)
297+
.putHeader(CONTENT_TYPE, APPLICATION_PROBLEM_JSON)
297298
.end(serializeResponse(error));
298299
} else if (streaming) {
299300
final Multi<? extends A2AResponse<?>> finalStreamingResponse = streamingResponse;
@@ -309,7 +310,7 @@ public void invokeJSONRPCHandler(@Body String body, RoutingContext rc) {
309310
} else {
310311
rc.response()
311312
.setStatusCode(200)
312-
.putHeader(CONTENT_TYPE, APPLICATION_JSON)
313+
.putHeader(CONTENT_TYPE, nonStreamingResponse.getError() != null ? APPLICATION_PROBLEM_JSON : APPLICATION_JSON)
313314
.end(serializeResponse(nonStreamingResponse));
314315
}
315316
}

reference/jsonrpc/src/test/java/io/a2a/server/apps/quarkus/A2AServerRoutesTest.java

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
package io.a2a.server.apps.quarkus;
22

3+
import static io.a2a.common.MediaType.APPLICATION_PROBLEM_JSON;
4+
import static io.a2a.spec.A2AMethods.DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
5+
import static io.a2a.spec.A2AMethods.GET_TASK_METHOD;
6+
import static io.a2a.spec.A2AMethods.GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
7+
import static io.a2a.spec.A2AMethods.LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
8+
import static io.a2a.spec.A2AMethods.SEND_MESSAGE_METHOD;
9+
import static io.a2a.spec.A2AMethods.SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
10+
import static io.a2a.spec.A2AMethods.SUBSCRIBE_TO_TASK_METHOD;
311
import static io.a2a.spec.A2AMethods.CANCEL_TASK_METHOD;
412
import static io.a2a.spec.A2AMethods.GET_EXTENDED_AGENT_CARD_METHOD;
513
import static io.a2a.spec.A2AMethods.SEND_STREAMING_MESSAGE_METHOD;
614
import static io.a2a.transport.jsonrpc.context.JSONRPCContextKeys.METHOD_NAME_KEY;
715
import static io.a2a.transport.jsonrpc.context.JSONRPCContextKeys.TENANT_KEY;
816
import static java.util.Collections.singletonList;
17+
import static io.vertx.core.http.HttpHeaders.CONTENT_TYPE;
18+
import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
919
import static org.junit.jupiter.api.Assertions.assertEquals;
1020
import static org.junit.jupiter.api.Assertions.assertNotNull;
1121
import static org.mockito.ArgumentMatchers.any;
@@ -59,14 +69,6 @@
5969
import org.junit.jupiter.api.Test;
6070
import org.mockito.ArgumentCaptor;
6171

62-
import static io.a2a.spec.A2AMethods.DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
63-
import static io.a2a.spec.A2AMethods.GET_TASK_METHOD;
64-
import static io.a2a.spec.A2AMethods.GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
65-
import static io.a2a.spec.A2AMethods.LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
66-
import static io.a2a.spec.A2AMethods.SEND_MESSAGE_METHOD;
67-
import static io.a2a.spec.A2AMethods.SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
68-
import static io.a2a.spec.A2AMethods.SUBSCRIBE_TO_TASK_METHOD;
69-
7072
/**
7173
* Unit test for JSON-RPC A2AServerRoutes that verifies the method names are properly set
7274
* in the ServerCallContext for all request types.
@@ -166,6 +168,7 @@ public void testSendMessage_MethodNameSetInContext() {
166168
ServerCallContext capturedContext = contextCaptor.getValue();
167169
assertNotNull(capturedContext);
168170
assertEquals(SEND_MESSAGE_METHOD, capturedContext.getState().get(METHOD_NAME_KEY));
171+
verify(mockHttpResponse).putHeader(CONTENT_TYPE, APPLICATION_JSON);
169172
}
170173

171174
@Test
@@ -250,6 +253,7 @@ public void testGetTask_MethodNameSetInContext() {
250253
ServerCallContext capturedContext = contextCaptor.getValue();
251254
assertNotNull(capturedContext);
252255
assertEquals(GET_TASK_METHOD, capturedContext.getState().get(METHOD_NAME_KEY));
256+
verify(mockHttpResponse).putHeader(CONTENT_TYPE, APPLICATION_JSON);
253257
}
254258

255259
@Test
@@ -286,6 +290,7 @@ public void testCancelTask_MethodNameSetInContext() {
286290
ServerCallContext capturedContext = contextCaptor.getValue();
287291
assertNotNull(capturedContext);
288292
assertEquals(CANCEL_TASK_METHOD, capturedContext.getState().get(METHOD_NAME_KEY));
293+
verify(mockHttpResponse).putHeader(CONTENT_TYPE, APPLICATION_JSON);
289294
}
290295

291296
@Test
@@ -363,6 +368,7 @@ public void testCreateTaskPushNotificationConfig_MethodNameSetInContext() {
363368
ServerCallContext capturedContext = contextCaptor.getValue();
364369
assertNotNull(capturedContext);
365370
assertEquals(SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, capturedContext.getState().get(METHOD_NAME_KEY));
371+
verify(mockHttpResponse).putHeader(CONTENT_TYPE, APPLICATION_JSON);
366372
}
367373

368374
@Test
@@ -401,6 +407,7 @@ public void testGetTaskPushNotificationConfig_MethodNameSetInContext() {
401407
ServerCallContext capturedContext = contextCaptor.getValue();
402408
assertNotNull(capturedContext);
403409
assertEquals(GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, capturedContext.getState().get(METHOD_NAME_KEY));
410+
verify(mockHttpResponse).putHeader(CONTENT_TYPE, APPLICATION_JSON);
404411
}
405412

406413
@Test
@@ -440,6 +447,7 @@ public void testListTaskPushNotificationConfig_MethodNameSetInContext() {
440447
ServerCallContext capturedContext = contextCaptor.getValue();
441448
assertNotNull(capturedContext);
442449
assertEquals(LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, capturedContext.getState().get(METHOD_NAME_KEY));
450+
verify(mockHttpResponse).putHeader(CONTENT_TYPE, APPLICATION_JSON);
443451
}
444452

445453
@Test
@@ -473,6 +481,7 @@ public void testDeleteTaskPushNotificationConfig_MethodNameSetInContext() {
473481
ServerCallContext capturedContext = contextCaptor.getValue();
474482
assertNotNull(capturedContext);
475483
assertEquals(DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, capturedContext.getState().get(METHOD_NAME_KEY));
484+
verify(mockHttpResponse).putHeader(CONTENT_TYPE, APPLICATION_JSON);
476485
}
477486

478487
@Test
@@ -509,6 +518,7 @@ public void testGetExtendedCard_MethodNameSetInContext() {
509518
ServerCallContext capturedContext = contextCaptor.getValue();
510519
assertNotNull(capturedContext);
511520
assertEquals(GET_EXTENDED_AGENT_CARD_METHOD, capturedContext.getState().get(METHOD_NAME_KEY));
521+
verify(mockHttpResponse).putHeader(CONTENT_TYPE, APPLICATION_JSON);
512522
}
513523

514524
@Test
@@ -707,6 +717,38 @@ public void testTenantExtraction_StreamingRequest() {
707717
assertEquals("myTenant/api", capturedContext.getState().get(TENANT_KEY));
708718
}
709719

720+
@Test
721+
public void testJsonParseError_ContentTypeIsProblemJson() {
722+
// Arrange - invalid JSON
723+
String invalidJson = "not valid json {{{";
724+
when(mockRequestBody.asString()).thenReturn(invalidJson);
725+
726+
// Act
727+
routes.invokeJSONRPCHandler(invalidJson, mockRoutingContext);
728+
729+
// Assert
730+
verify(mockHttpResponse).putHeader(CONTENT_TYPE, APPLICATION_PROBLEM_JSON);
731+
}
732+
733+
@Test
734+
public void testMethodNotFound_ContentTypeIsProblemJson() {
735+
// Arrange - unknown method
736+
String jsonRpcRequest = """
737+
{
738+
"jsonrpc": "2.0",
739+
"id": "cd4c76de-d54c-436c-8b9f-4c2703648d64",
740+
"method": "UnknownMethod",
741+
"params": {}
742+
}""";
743+
when(mockRequestBody.asString()).thenReturn(jsonRpcRequest);
744+
745+
// Act
746+
routes.invokeJSONRPCHandler(jsonRpcRequest, mockRoutingContext);
747+
748+
// Assert
749+
verify(mockHttpResponse).putHeader(CONTENT_TYPE, APPLICATION_PROBLEM_JSON);
750+
}
751+
710752
/**
711753
* Helper method to set a field via reflection for testing purposes.
712754
*/

transport/rest/src/main/java/io/a2a/transport/rest/handler/RestHandler.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.a2a.transport.rest.handler;
22

3+
import static io.a2a.common.MediaType.APPLICATION_JSON;
4+
import static io.a2a.common.MediaType.APPLICATION_PROBLEM_JSON;
35
import static io.a2a.server.util.async.AsyncUtils.createTubeConfig;
46
import static io.a2a.spec.A2AErrorCodes.JSON_PARSE_ERROR_CODE;
57

@@ -115,6 +117,7 @@ public class RestHandler {
115117
private static final Logger log = Logger.getLogger(RestHandler.class.getName());
116118
private static final String TASK_STATE_PREFIX = "TASK_STATE_";
117119

120+
118121
// Fields set by constructor injection cannot be final. We need a noargs constructor for
119122
// Jakarta compatibility, and it seems that making fields set by constructor injection
120123
// final, is not proxyable in all runtimes
@@ -589,7 +592,7 @@ public HTTPRestResponse deleteTaskPushNotificationConfiguration(ServerCallContex
589592
}
590593
DeleteTaskPushNotificationConfigParams params = new DeleteTaskPushNotificationConfigParams(taskId, configId, tenant);
591594
requestHandler.onDeleteTaskPushNotificationConfig(params, context);
592-
return new HTTPRestResponse(204, "application/json", "");
595+
return new HTTPRestResponse(204, APPLICATION_JSON, "");
593596
} catch (A2AError e) {
594597
return createErrorResponse(e);
595598
} catch (Throwable throwable) {
@@ -622,8 +625,8 @@ private void validate(String json) {
622625
private HTTPRestResponse createSuccessResponse(int statusCode, com.google.protobuf.Message.Builder builder) {
623626
try {
624627
// Include default value fields to ensure empty arrays, zeros, etc. are present in JSON
625-
String jsonBody = JsonFormat.printer().includingDefaultValueFields().print(builder);
626-
return new HTTPRestResponse(statusCode, "application/json", jsonBody);
628+
String jsonBody = JsonFormat.printer().alwaysPrintFieldsWithNoPresence().print(builder);
629+
return new HTTPRestResponse(statusCode, APPLICATION_JSON, jsonBody);
627630
} catch (InvalidProtocolBufferException e) {
628631
return createErrorResponse(new InternalError("Failed to serialize response: " + e.getMessage()));
629632
}
@@ -642,7 +645,7 @@ public HTTPRestResponse createErrorResponse(A2AError error) {
642645

643646
private HTTPRestResponse createErrorResponse(int statusCode, A2AError error) {
644647
String jsonBody = new HTTPRestErrorResponse(error).toJson();
645-
return new HTTPRestResponse(statusCode, "application/json", jsonBody);
648+
return new HTTPRestResponse(statusCode, APPLICATION_PROBLEM_JSON, jsonBody);
646649
}
647650

648651
private HTTPRestStreamingResponse createStreamingResponse(Flow.Publisher<StreamingEventKind> publisher) {
@@ -789,7 +792,7 @@ public HTTPRestResponse getExtendedAgentCard(ServerCallContext context, String t
789792
if (!agentCard.capabilities().extendedAgentCard() || extendedAgentCard == null || !extendedAgentCard.isResolvable()) {
790793
throw new ExtendedAgentCardNotConfiguredError(null, "Extended Card not configured", null);
791794
}
792-
return new HTTPRestResponse(200, "application/json", JsonUtil.toJson(extendedAgentCard.get()));
795+
return new HTTPRestResponse(200, APPLICATION_JSON, JsonUtil.toJson(extendedAgentCard.get()));
793796
} catch (A2AError e) {
794797
return createErrorResponse(e);
795798
} catch (Throwable t) {
@@ -831,7 +834,7 @@ public HTTPRestResponse getExtendedAgentCard(ServerCallContext context, String t
831834
*/
832835
public HTTPRestResponse getAgentCard() {
833836
try {
834-
return new HTTPRestResponse(200, "application/json", JsonUtil.toJson(agentCard));
837+
return new HTTPRestResponse(200, APPLICATION_JSON, JsonUtil.toJson(agentCard));
835838
} catch (Throwable t) {
836839
return createErrorResponse(500, new InternalError(t.getMessage()));
837840
}

transport/rest/src/test/java/io/a2a/transport/rest/handler/RestHandlerTest.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public void testGetTaskNotFound() {
5454
RestHandler.HTTPRestResponse response = handler.getTask(callContext, "", "nonexistent", 0);
5555

5656
Assertions.assertEquals(404, response.getStatusCode());
57-
Assertions.assertEquals("application/json", response.getContentType());
57+
Assertions.assertEquals("application/problem+json", response.getContentType());
5858
Assertions.assertTrue(response.getBody().contains("TaskNotFoundError"));
5959
}
6060

@@ -79,7 +79,7 @@ public void testListTasksInvalidStatus() {
7979
null, null, null);
8080

8181
Assertions.assertEquals(422, response.getStatusCode());
82-
Assertions.assertEquals("application/json", response.getContentType());
82+
Assertions.assertEquals("application/problem+json", response.getContentType());
8383
Assertions.assertTrue(response.getBody().contains("InvalidParamsError"));
8484
}
8585

@@ -122,7 +122,7 @@ public void testSendMessageInvalidBody() {
122122
RestHandler.HTTPRestResponse response = handler.sendMessage(callContext, "", invalidBody);
123123

124124
Assertions.assertEquals(400, response.getStatusCode());
125-
Assertions.assertEquals("application/json", response.getContentType());
125+
Assertions.assertEquals("application/problem+json", response.getContentType());
126126
Assertions.assertTrue(response.getBody().contains("JSONParseError"),response.getBody());
127127
}
128128

@@ -146,7 +146,7 @@ public void testSendMessageWrongValueBody() {
146146
RestHandler.HTTPRestResponse response = handler.sendMessage(callContext, "", requestBody);
147147

148148
Assertions.assertEquals(422, response.getStatusCode());
149-
Assertions.assertEquals("application/json", response.getContentType());
149+
Assertions.assertEquals("application/problem+json", response.getContentType());
150150
Assertions.assertTrue(response.getBody().contains("InvalidParamsError"));
151151
}
152152

@@ -157,7 +157,7 @@ public void testSendMessageEmptyBody() {
157157
RestHandler.HTTPRestResponse response = handler.sendMessage(callContext, "", "");
158158

159159
Assertions.assertEquals(400, response.getStatusCode());
160-
Assertions.assertEquals("application/json", response.getContentType());
160+
Assertions.assertEquals("application/problem+json", response.getContentType());
161161
Assertions.assertTrue(response.getBody().contains("InvalidRequestError"));
162162
}
163163

@@ -190,7 +190,7 @@ public void testCancelTaskNotFound() {
190190
RestHandler.HTTPRestResponse response = handler.cancelTask(callContext, "", requestBody, "nonexistent");
191191

192192
Assertions.assertEquals(404, response.getStatusCode());
193-
Assertions.assertEquals("application/json", response.getContentType());
193+
Assertions.assertEquals("application/problem+json", response.getContentType());
194194
Assertions.assertTrue(response.getBody().contains("TaskNotFoundError"));
195195
}
196196

@@ -564,7 +564,7 @@ public void testExtensionSupportRequiredErrorOnSendMessage() {
564564
RestHandler.HTTPRestResponse response = handler.sendMessage(callContext, "", requestBody);
565565

566566
Assertions.assertEquals(400, response.getStatusCode());
567-
Assertions.assertEquals("application/json", response.getContentType());
567+
Assertions.assertEquals("application/problem+json", response.getContentType());
568568
Assertions.assertTrue(response.getBody().contains("ExtensionSupportRequiredError"));
569569
Assertions.assertTrue(response.getBody().contains("https://example.com/test-extension"));
570570
}
@@ -764,7 +764,7 @@ public void testVersionNotSupportedErrorOnSendMessage() {
764764
RestHandler.HTTPRestResponse response = handler.sendMessage(contextWithVersion, "", requestBody);
765765

766766
Assertions.assertEquals(501, response.getStatusCode());
767-
Assertions.assertEquals("application/json", response.getContentType());
767+
Assertions.assertEquals("application/problem+json", response.getContentType());
768768
Assertions.assertTrue(response.getBody().contains("VersionNotSupportedError"));
769769
Assertions.assertTrue(response.getBody().contains("2.0"));
770770
}
@@ -969,7 +969,7 @@ public void testListTasksNegativeTimestampReturns422() {
969969
null, "-1", null);
970970

971971
Assertions.assertEquals(422, response.getStatusCode());
972-
Assertions.assertEquals("application/json", response.getContentType());
972+
Assertions.assertEquals("application/problem+json", response.getContentType());
973973
Assertions.assertTrue(response.getBody().contains("InvalidParamsError"));
974974
}
975975

0 commit comments

Comments
 (0)