Skip to content

Commit c8ba7df

Browse files
authored
fix: use application/json Content-Type for HTTP+JSON error responses (#769)
The spec is not explicit on that but the error responses should all use `application/json`. * in JSON-RPC, an error response is a regular JSON-RPC response * in HTTP+JSON, the error payload was moved from RFC 9457 to AIP-193 so the RFC-9457's `application/problem+json` no longer makes sense. This fixes #768 --------- Signed-off-by: Jeff Mesnil <jmesnil@ibm.com>
1 parent ed23f55 commit c8ba7df

6 files changed

Lines changed: 54 additions & 53 deletions

File tree

common/src/main/java/io/a2a/common/MediaType.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,5 @@
22

33
public interface MediaType {
44

5-
public static final String APPLICATION_JSON = "application/json";
6-
public static final String APPLICATION_PROBLEM_JSON = "application/problem+json";
5+
String APPLICATION_JSON = "application/json";
76
}

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

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

3-
import static io.a2a.common.MediaType.APPLICATION_PROBLEM_JSON;
43
import static io.a2a.server.ServerCallContext.TRANSPORT_KEY;
54
import static io.a2a.transport.jsonrpc.context.JSONRPCContextKeys.HEADERS_KEY;
65
import static io.a2a.transport.jsonrpc.context.JSONRPCContextKeys.METHOD_NAME_KEY;
@@ -248,7 +247,7 @@ public class A2AServerRoutes {
248247
*
249248
* <p><b>Processing Flow:</b>
250249
* <ol>
251-
* <li>Parse JSON-RPC request body using {@link JSONRPCUtils#parseRequestBody}</li>
250+
* <li>Parse JSON-RPC request body using {@link JSONRPCUtils#parseRequestBody(String, String)}</li>
252251
* <li>Create {@link ServerCallContext} from routing context</li>
253252
* <li>Route to streaming or non-streaming handler</li>
254253
* <li>Handle errors with appropriate JSON-RPC error codes</li>
@@ -297,7 +296,7 @@ public void invokeJSONRPCHandler(@Body String body, RoutingContext rc) {
297296
if (error != null) {
298297
rc.response()
299298
.setStatusCode(200)
300-
.putHeader(CONTENT_TYPE, APPLICATION_PROBLEM_JSON)
299+
.putHeader(CONTENT_TYPE, APPLICATION_JSON)
301300
.end(serializeResponse(error));
302301
} else if (streaming) {
303302
final Multi<? extends A2AResponse<?>> finalStreamingResponse = streamingResponse;
@@ -313,7 +312,7 @@ public void invokeJSONRPCHandler(@Body String body, RoutingContext rc) {
313312
} else {
314313
rc.response()
315314
.setStatusCode(200)
316-
.putHeader(CONTENT_TYPE, nonStreamingResponse.getError() != null ? APPLICATION_PROBLEM_JSON : APPLICATION_JSON)
315+
.putHeader(CONTENT_TYPE, APPLICATION_JSON)
317316
.end(serializeResponse(nonStreamingResponse));
318317
}
319318
}

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

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

3-
import static io.a2a.common.MediaType.APPLICATION_PROBLEM_JSON;
43
import static io.a2a.spec.A2AMethods.DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
54
import static io.a2a.spec.A2AMethods.GET_TASK_METHOD;
65
import static io.a2a.spec.A2AMethods.GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
@@ -718,7 +717,7 @@ public void testTenantExtraction_StreamingRequest() {
718717
}
719718

720719
@Test
721-
public void testJsonParseError_ContentTypeIsProblemJson() {
720+
public void testJsonParseError_ContentTypeIsApplicationJson() {
722721
// Arrange - invalid JSON
723722
String invalidJson = "not valid json {{{";
724723
when(mockRequestBody.asString()).thenReturn(invalidJson);
@@ -727,11 +726,11 @@ public void testJsonParseError_ContentTypeIsProblemJson() {
727726
routes.invokeJSONRPCHandler(invalidJson, mockRoutingContext);
728727

729728
// Assert
730-
verify(mockHttpResponse).putHeader(CONTENT_TYPE, APPLICATION_PROBLEM_JSON);
729+
verify(mockHttpResponse).putHeader(CONTENT_TYPE, APPLICATION_JSON);
731730
}
732731

733732
@Test
734-
public void testMethodNotFound_ContentTypeIsProblemJson() {
733+
public void testMethodNotFound_ContentTypeIsApplicationJson() {
735734
// Arrange - unknown method
736735
String jsonRpcRequest = """
737736
{
@@ -746,7 +745,7 @@ public void testMethodNotFound_ContentTypeIsProblemJson() {
746745
routes.invokeJSONRPCHandler(jsonRpcRequest, mockRoutingContext);
747746

748747
// Assert
749-
verify(mockHttpResponse).putHeader(CONTENT_TYPE, APPLICATION_PROBLEM_JSON);
748+
verify(mockHttpResponse).putHeader(CONTENT_TYPE, APPLICATION_JSON);
750749
}
751750

752751
/**

reference/rest/src/test/java/io/a2a/server/rest/quarkus/A2AServerRoutesTest.java

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

3+
import static io.a2a.common.MediaType.APPLICATION_JSON;
34
import static io.a2a.spec.A2AMethods.CANCEL_TASK_METHOD;
45
import static io.a2a.spec.A2AMethods.DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
56
import static io.a2a.spec.A2AMethods.GET_TASK_METHOD;
@@ -82,7 +83,7 @@ public void setUp() {
8283
when(mockRoutingContext.user()).thenReturn(null);
8384
when(mockRequest.headers()).thenReturn(mockHeaders);
8485
when(mockRequest.params()).thenReturn(mockParams);
85-
when(mockRequest.getHeader(any(CharSequence.class))).thenReturn("application/json");
86+
when(mockRequest.getHeader(any(CharSequence.class))).thenReturn(APPLICATION_JSON);
8687
when(mockRoutingContext.body()).thenReturn(mockRequestBody);
8788
when(mockRequestBody.asString()).thenReturn("{}");
8889
when(mockResponse.setStatusCode(any(Integer.class))).thenReturn(mockResponse);
@@ -96,7 +97,7 @@ public void testSendMessage_MethodNameSetInContext() {
9697
// Arrange
9798
HTTPRestResponse mockHttpResponse = mock(HTTPRestResponse.class);
9899
when(mockHttpResponse.getStatusCode()).thenReturn(200);
99-
when(mockHttpResponse.getContentType()).thenReturn("application/json");
100+
when(mockHttpResponse.getContentType()).thenReturn(APPLICATION_JSON);
100101
when(mockHttpResponse.getBody()).thenReturn("{}");
101102
when(mockRestHandler.sendMessage(any(ServerCallContext.class), anyString(), anyString())).thenReturn(mockHttpResponse);
102103

@@ -117,7 +118,7 @@ public void testSendMessageStreaming_MethodNameSetInContext() {
117118
// Arrange
118119
HTTPRestResponse mockHttpResponse = mock(HTTPRestResponse.class);
119120
when(mockHttpResponse.getStatusCode()).thenReturn(200);
120-
when(mockHttpResponse.getContentType()).thenReturn("application/json");
121+
when(mockHttpResponse.getContentType()).thenReturn(APPLICATION_JSON);
121122
when(mockHttpResponse.getBody()).thenReturn("{}");
122123
when(mockRestHandler.sendStreamingMessage(any(ServerCallContext.class), anyString(), anyString()))
123124
.thenReturn(mockHttpResponse);
@@ -140,7 +141,7 @@ public void testGetTask_MethodNameSetInContext() {
140141
when(mockRoutingContext.pathParam("taskId")).thenReturn("task123");
141142
HTTPRestResponse mockHttpResponse = mock(HTTPRestResponse.class);
142143
when(mockHttpResponse.getStatusCode()).thenReturn(200);
143-
when(mockHttpResponse.getContentType()).thenReturn("application/json");
144+
when(mockHttpResponse.getContentType()).thenReturn(APPLICATION_JSON);
144145
when(mockHttpResponse.getBody()).thenReturn("{test:value}");
145146
when(mockRestHandler.getTask(any(ServerCallContext.class), anyString(), anyString(), any())).thenReturn(mockHttpResponse);
146147

@@ -162,7 +163,7 @@ public void testCancelTask_MethodNameSetInContext() {
162163
when(mockRoutingContext.pathParam("taskId")).thenReturn("task123");
163164
HTTPRestResponse mockHttpResponse = mock(HTTPRestResponse.class);
164165
when(mockHttpResponse.getStatusCode()).thenReturn(200);
165-
when(mockHttpResponse.getContentType()).thenReturn("application/json");
166+
when(mockHttpResponse.getContentType()).thenReturn(APPLICATION_JSON);
166167
when(mockHttpResponse.getBody()).thenReturn("{}");
167168
when(mockRestHandler.cancelTask(any(ServerCallContext.class), anyString(), anyString(), anyString())).thenReturn(mockHttpResponse);
168169

@@ -184,7 +185,7 @@ public void testCancelTask_WithMetadata() {
184185
when(mockRoutingContext.pathParam("taskId")).thenReturn("task456");
185186
HTTPRestResponse mockHttpResponse = mock(HTTPRestResponse.class);
186187
when(mockHttpResponse.getStatusCode()).thenReturn(200);
187-
when(mockHttpResponse.getContentType()).thenReturn("application/json");
188+
when(mockHttpResponse.getContentType()).thenReturn(APPLICATION_JSON);
188189
when(mockHttpResponse.getBody()).thenReturn("{\"id\":\"task456\",\"status\":\"cancelled\"}");
189190

190191
String requestBody = """
@@ -217,7 +218,7 @@ public void testCancelTask_WithEmptyMetadata() {
217218
when(mockRoutingContext.pathParam("taskId")).thenReturn("task789");
218219
HTTPRestResponse mockHttpResponse = mock(HTTPRestResponse.class);
219220
when(mockHttpResponse.getStatusCode()).thenReturn(200);
220-
when(mockHttpResponse.getContentType()).thenReturn("application/json");
221+
when(mockHttpResponse.getContentType()).thenReturn(APPLICATION_JSON);
221222
when(mockHttpResponse.getBody()).thenReturn("{\"id\":\"task789\"}");
222223

223224
String requestBody = """
@@ -246,7 +247,7 @@ public void testCancelTask_WithNoMetadataField() {
246247
when(mockRoutingContext.pathParam("taskId")).thenReturn("task999");
247248
HTTPRestResponse mockHttpResponse = mock(HTTPRestResponse.class);
248249
when(mockHttpResponse.getStatusCode()).thenReturn(200);
249-
when(mockHttpResponse.getContentType()).thenReturn("application/json");
250+
when(mockHttpResponse.getContentType()).thenReturn(APPLICATION_JSON);
250251
when(mockHttpResponse.getBody()).thenReturn("{\"id\":\"task999\"}");
251252

252253
String requestBody = "{}";
@@ -271,7 +272,7 @@ public void testCancelTask_WithNullBody() {
271272
when(mockRoutingContext.pathParam("taskId")).thenReturn("task111");
272273
HTTPRestResponse mockHttpResponse = mock(HTTPRestResponse.class);
273274
when(mockHttpResponse.getStatusCode()).thenReturn(200);
274-
when(mockHttpResponse.getContentType()).thenReturn("application/json");
275+
when(mockHttpResponse.getContentType()).thenReturn(APPLICATION_JSON);
275276
when(mockHttpResponse.getBody()).thenReturn("{\"id\":\"task111\"}");
276277

277278
ArgumentCaptor<String> bodyCaptor = ArgumentCaptor.forClass(String.class);
@@ -293,7 +294,7 @@ public void testCancelTask_WithComplexMetadata() {
293294
when(mockRoutingContext.pathParam("taskId")).thenReturn("task222");
294295
HTTPRestResponse mockHttpResponse = mock(HTTPRestResponse.class);
295296
when(mockHttpResponse.getStatusCode()).thenReturn(200);
296-
when(mockHttpResponse.getContentType()).thenReturn("application/json");
297+
when(mockHttpResponse.getContentType()).thenReturn(APPLICATION_JSON);
297298
when(mockHttpResponse.getBody()).thenReturn("{\"id\":\"task222\"}");
298299

299300
String requestBody = """
@@ -331,7 +332,7 @@ public void testSubscribeTask_MethodNameSetInContext() {
331332
when(mockRoutingContext.pathParam("taskId")).thenReturn("task123");
332333
HTTPRestResponse mockHttpResponse = mock(HTTPRestResponse.class);
333334
when(mockHttpResponse.getStatusCode()).thenReturn(200);
334-
when(mockHttpResponse.getContentType()).thenReturn("application/json");
335+
when(mockHttpResponse.getContentType()).thenReturn(APPLICATION_JSON);
335336
when(mockHttpResponse.getBody()).thenReturn("{}");
336337
when(mockRestHandler.subscribeToTask(any(ServerCallContext.class), anyString(), anyString()))
337338
.thenReturn(mockHttpResponse);
@@ -354,7 +355,7 @@ public void testCreateTaskPushNotificationConfiguration_MethodNameSetInContext()
354355
when(mockRoutingContext.pathParam("taskId")).thenReturn("task123");
355356
HTTPRestResponse mockHttpResponse = mock(HTTPRestResponse.class);
356357
when(mockHttpResponse.getStatusCode()).thenReturn(200);
357-
when(mockHttpResponse.getContentType()).thenReturn("application/json");
358+
when(mockHttpResponse.getContentType()).thenReturn(APPLICATION_JSON);
358359
when(mockHttpResponse.getBody()).thenReturn("{}");
359360
when(mockRestHandler.createTaskPushNotificationConfiguration(any(ServerCallContext.class), anyString(), anyString(), anyString())).thenReturn(mockHttpResponse);
360361

@@ -377,7 +378,7 @@ public void testGetTaskPushNotificationConfiguration_MethodNameSetInContext() {
377378
when(mockRoutingContext.pathParam("configId")).thenReturn("config456");
378379
HTTPRestResponse mockHttpResponse = mock(HTTPRestResponse.class);
379380
when(mockHttpResponse.getStatusCode()).thenReturn(200);
380-
when(mockHttpResponse.getContentType()).thenReturn("application/json");
381+
when(mockHttpResponse.getContentType()).thenReturn(APPLICATION_JSON);
381382
when(mockHttpResponse.getBody()).thenReturn("{}");
382383
when(mockRestHandler.getTaskPushNotificationConfiguration(any(ServerCallContext.class), anyString(), anyString(), anyString())).thenReturn(mockHttpResponse);
383384

@@ -400,7 +401,7 @@ public void testListTaskPushNotificationConfigurations_MethodNameSetInContext()
400401
when(mockRoutingContext.pathParam("taskId")).thenReturn("task123");
401402
HTTPRestResponse mockHttpResponse = mock(HTTPRestResponse.class);
402403
when(mockHttpResponse.getStatusCode()).thenReturn(200);
403-
when(mockHttpResponse.getContentType()).thenReturn("application/json");
404+
when(mockHttpResponse.getContentType()).thenReturn(APPLICATION_JSON);
404405
when(mockHttpResponse.getBody()).thenReturn("{}");
405406
when(mockRestHandler.listTaskPushNotificationConfigurations(any(ServerCallContext.class), anyString(), anyString(), anyInt(), anyString()))
406407
.thenReturn(mockHttpResponse);
@@ -424,7 +425,7 @@ public void testDeleteTaskPushNotificationConfiguration_MethodNameSetInContext()
424425
when(mockRoutingContext.pathParam("configId")).thenReturn("config456");
425426
HTTPRestResponse mockHttpResponse = mock(HTTPRestResponse.class);
426427
when(mockHttpResponse.getStatusCode()).thenReturn(200);
427-
when(mockHttpResponse.getContentType()).thenReturn("application/json");
428+
when(mockHttpResponse.getContentType()).thenReturn(APPLICATION_JSON);
428429
when(mockHttpResponse.getBody()).thenReturn("{}");
429430
when(mockRestHandler.deleteTaskPushNotificationConfiguration(any(ServerCallContext.class), anyString(), anyString(), anyString())).thenReturn(mockHttpResponse);
430431

@@ -446,7 +447,7 @@ public void testSendMessage_UnsupportedContentType_ReturnsContentTypeNotSupporte
446447
// Arrange
447448
HTTPRestResponse mockErrorResponse = mock(HTTPRestResponse.class);
448449
when(mockErrorResponse.getStatusCode()).thenReturn(415);
449-
when(mockErrorResponse.getContentType()).thenReturn("application/problem+json");
450+
when(mockErrorResponse.getContentType()).thenReturn(APPLICATION_JSON);
450451
when(mockErrorResponse.getBody()).thenReturn("{\"error\":{\"code\":415,\"status\":\"INVALID_ARGUMENT\",\"message\":\"Incompatible content types\",\"details\":[{\"reason\":\"CONTENT_TYPE_NOT_SUPPORTED\",\"domain\":\"a2a-protocol.org\"}]}}");
451452
when(mockRestHandler.createErrorResponse(any(ContentTypeNotSupportedError.class))).thenReturn(mockErrorResponse);
452453
when(mockRequest.getHeader(any(CharSequence.class))).thenReturn("text/plain");
@@ -464,7 +465,7 @@ public void testSendMessageStreaming_UnsupportedContentType_ReturnsContentTypeNo
464465
// Arrange
465466
HTTPRestResponse mockErrorResponse = mock(HTTPRestResponse.class);
466467
when(mockErrorResponse.getStatusCode()).thenReturn(415);
467-
when(mockErrorResponse.getContentType()).thenReturn("application/problem+json");
468+
when(mockErrorResponse.getContentType()).thenReturn(APPLICATION_JSON);
468469
when(mockErrorResponse.getBody()).thenReturn("{\"error\":{\"code\":415,\"status\":\"INVALID_ARGUMENT\",\"message\":\"Incompatible content types\",\"details\":[{\"reason\":\"CONTENT_TYPE_NOT_SUPPORTED\",\"domain\":\"a2a-protocol.org\"}]}}");
469470
when(mockRestHandler.createErrorResponse(any(ContentTypeNotSupportedError.class))).thenReturn(mockErrorResponse);
470471
when(mockRequest.getHeader(any(CharSequence.class))).thenReturn("text/plain");
@@ -482,9 +483,9 @@ public void testSendMessage_UnsupportedProtocolVersion_ReturnsVersionNotSupporte
482483
// Arrange: content type is OK, but RestHandler returns a VersionNotSupportedError response
483484
HTTPRestResponse mockErrorResponse = mock(HTTPRestResponse.class);
484485
when(mockErrorResponse.getStatusCode()).thenReturn(400);
485-
when(mockErrorResponse.getContentType()).thenReturn("application/problem+json");
486+
when(mockErrorResponse.getContentType()).thenReturn(APPLICATION_JSON);
486487
when(mockErrorResponse.getBody()).thenReturn("{\"error\":{\"code\":400,\"status\":\"UNIMPLEMENTED\",\"message\":\"Protocol version not supported\",\"details\":[{\"reason\":\"VERSION_NOT_SUPPORTED\",\"domain\":\"a2a-protocol.org\"}]}}");
487-
when(mockRequest.getHeader(any(CharSequence.class))).thenReturn("application/json");
488+
when(mockRequest.getHeader(any(CharSequence.class))).thenReturn(APPLICATION_JSON);
488489
when(mockRestHandler.sendMessage(any(ServerCallContext.class), anyString(), anyString()))
489490
.thenReturn(mockErrorResponse);
490491

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

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

33
import static io.a2a.common.MediaType.APPLICATION_JSON;
4-
import static io.a2a.common.MediaType.APPLICATION_PROBLEM_JSON;
54
import static io.a2a.server.util.async.AsyncUtils.createTubeConfig;
65

76
import io.a2a.spec.A2AErrorCodes;
@@ -683,7 +682,7 @@ public HTTPRestResponse createErrorResponse(A2AError error) {
683682

684683
private HTTPRestResponse createErrorResponse(int statusCode, A2AError error) {
685684
String jsonBody = new HTTPRestErrorResponse(error).toJson();
686-
return new HTTPRestResponse(statusCode, APPLICATION_PROBLEM_JSON, jsonBody);
685+
return new HTTPRestResponse(statusCode, APPLICATION_JSON, jsonBody);
687686
}
688687

689688
private HTTPRestStreamingResponse createStreamingResponse(Flow.Publisher<StreamingEventKind> publisher) {

0 commit comments

Comments
 (0)