Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions common/src/main/java/io/a2a/common/MediaType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.a2a.common;

public interface MediaType {

public static final String APPLICATION_JSON = "application/json";
public static final String APPLICATION_PROBLEM_JSON = "application/problem+json";
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.a2a.server.apps.quarkus;

import static io.a2a.common.MediaType.APPLICATION_PROBLEM_JSON;
import static io.a2a.server.ServerCallContext.TRANSPORT_KEY;
import static io.a2a.transport.jsonrpc.context.JSONRPCContextKeys.HEADERS_KEY;
import static io.a2a.transport.jsonrpc.context.JSONRPCContextKeys.METHOD_NAME_KEY;
Expand Down Expand Up @@ -134,7 +135,7 @@ public void invokeJSONRPCHandler(@Body String body, RoutingContext rc) {
if (error != null) {
rc.response()
.setStatusCode(200)
.putHeader(CONTENT_TYPE, APPLICATION_JSON)
.putHeader(CONTENT_TYPE, APPLICATION_PROBLEM_JSON)
.end(serializeResponse(error));
} else if (streaming) {
final Multi<? extends A2AResponse<?>> finalStreamingResponse = streamingResponse;
Expand All @@ -150,7 +151,7 @@ public void invokeJSONRPCHandler(@Body String body, RoutingContext rc) {
} else {
rc.response()
.setStatusCode(200)
.putHeader(CONTENT_TYPE, APPLICATION_JSON)
.putHeader(CONTENT_TYPE, nonStreamingResponse.getError() != null ? APPLICATION_PROBLEM_JSON : APPLICATION_JSON)
.end(serializeResponse(nonStreamingResponse));
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
package io.a2a.server.apps.quarkus;

import static io.a2a.common.MediaType.APPLICATION_PROBLEM_JSON;
import static io.a2a.spec.A2AMethods.DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
import static io.a2a.spec.A2AMethods.GET_TASK_METHOD;
import static io.a2a.spec.A2AMethods.GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
import static io.a2a.spec.A2AMethods.LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
import static io.a2a.spec.A2AMethods.SEND_MESSAGE_METHOD;
import static io.a2a.spec.A2AMethods.SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
import static io.a2a.spec.A2AMethods.SUBSCRIBE_TO_TASK_METHOD;
import static io.a2a.spec.A2AMethods.CANCEL_TASK_METHOD;
import static io.a2a.spec.A2AMethods.GET_EXTENDED_AGENT_CARD_METHOD;
import static io.a2a.spec.A2AMethods.SEND_STREAMING_MESSAGE_METHOD;
import static io.a2a.transport.jsonrpc.context.JSONRPCContextKeys.METHOD_NAME_KEY;
import static io.a2a.transport.jsonrpc.context.JSONRPCContextKeys.TENANT_KEY;
import static java.util.Collections.singletonList;
import static io.vertx.core.http.HttpHeaders.CONTENT_TYPE;
import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
Expand Down Expand Up @@ -59,14 +69,6 @@
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;

import static io.a2a.spec.A2AMethods.DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
import static io.a2a.spec.A2AMethods.GET_TASK_METHOD;
import static io.a2a.spec.A2AMethods.GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
import static io.a2a.spec.A2AMethods.LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
import static io.a2a.spec.A2AMethods.SEND_MESSAGE_METHOD;
import static io.a2a.spec.A2AMethods.SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD;
import static io.a2a.spec.A2AMethods.SUBSCRIBE_TO_TASK_METHOD;

/**
* Unit test for JSON-RPC A2AServerRoutes that verifies the method names are properly set
* in the ServerCallContext for all request types.
Expand Down Expand Up @@ -166,6 +168,7 @@ public void testSendMessage_MethodNameSetInContext() {
ServerCallContext capturedContext = contextCaptor.getValue();
assertNotNull(capturedContext);
assertEquals(SEND_MESSAGE_METHOD, capturedContext.getState().get(METHOD_NAME_KEY));
verify(mockHttpResponse).putHeader(CONTENT_TYPE, APPLICATION_JSON);
}

@Test
Expand Down Expand Up @@ -250,6 +253,7 @@ public void testGetTask_MethodNameSetInContext() {
ServerCallContext capturedContext = contextCaptor.getValue();
assertNotNull(capturedContext);
assertEquals(GET_TASK_METHOD, capturedContext.getState().get(METHOD_NAME_KEY));
verify(mockHttpResponse).putHeader(CONTENT_TYPE, APPLICATION_JSON);
}

@Test
Expand Down Expand Up @@ -286,6 +290,7 @@ public void testCancelTask_MethodNameSetInContext() {
ServerCallContext capturedContext = contextCaptor.getValue();
assertNotNull(capturedContext);
assertEquals(CANCEL_TASK_METHOD, capturedContext.getState().get(METHOD_NAME_KEY));
verify(mockHttpResponse).putHeader(CONTENT_TYPE, APPLICATION_JSON);
}

@Test
Expand Down Expand Up @@ -363,6 +368,7 @@ public void testCreateTaskPushNotificationConfig_MethodNameSetInContext() {
ServerCallContext capturedContext = contextCaptor.getValue();
assertNotNull(capturedContext);
assertEquals(SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, capturedContext.getState().get(METHOD_NAME_KEY));
verify(mockHttpResponse).putHeader(CONTENT_TYPE, APPLICATION_JSON);
}

@Test
Expand Down Expand Up @@ -401,6 +407,7 @@ public void testGetTaskPushNotificationConfig_MethodNameSetInContext() {
ServerCallContext capturedContext = contextCaptor.getValue();
assertNotNull(capturedContext);
assertEquals(GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, capturedContext.getState().get(METHOD_NAME_KEY));
verify(mockHttpResponse).putHeader(CONTENT_TYPE, APPLICATION_JSON);
}

@Test
Expand Down Expand Up @@ -440,6 +447,7 @@ public void testListTaskPushNotificationConfig_MethodNameSetInContext() {
ServerCallContext capturedContext = contextCaptor.getValue();
assertNotNull(capturedContext);
assertEquals(LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, capturedContext.getState().get(METHOD_NAME_KEY));
verify(mockHttpResponse).putHeader(CONTENT_TYPE, APPLICATION_JSON);
}

@Test
Expand Down Expand Up @@ -473,6 +481,7 @@ public void testDeleteTaskPushNotificationConfig_MethodNameSetInContext() {
ServerCallContext capturedContext = contextCaptor.getValue();
assertNotNull(capturedContext);
assertEquals(DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, capturedContext.getState().get(METHOD_NAME_KEY));
verify(mockHttpResponse).putHeader(CONTENT_TYPE, APPLICATION_JSON);
}

@Test
Expand Down Expand Up @@ -509,6 +518,7 @@ public void testGetExtendedCard_MethodNameSetInContext() {
ServerCallContext capturedContext = contextCaptor.getValue();
assertNotNull(capturedContext);
assertEquals(GET_EXTENDED_AGENT_CARD_METHOD, capturedContext.getState().get(METHOD_NAME_KEY));
verify(mockHttpResponse).putHeader(CONTENT_TYPE, APPLICATION_JSON);
}

@Test
Expand Down Expand Up @@ -707,6 +717,38 @@ public void testTenantExtraction_StreamingRequest() {
assertEquals("myTenant/api", capturedContext.getState().get(TENANT_KEY));
}

@Test
public void testJsonParseError_ContentTypeIsProblemJson() {
// Arrange - invalid JSON
String invalidJson = "not valid json {{{";
when(mockRequestBody.asString()).thenReturn(invalidJson);

// Act
routes.invokeJSONRPCHandler(invalidJson, mockRoutingContext);

// Assert
verify(mockHttpResponse).putHeader(CONTENT_TYPE, APPLICATION_PROBLEM_JSON);
}

@Test
public void testMethodNotFound_ContentTypeIsProblemJson() {
// Arrange - unknown method
String jsonRpcRequest = """
{
"jsonrpc": "2.0",
"id": "cd4c76de-d54c-436c-8b9f-4c2703648d64",
"method": "UnknownMethod",
"params": {}
}""";
when(mockRequestBody.asString()).thenReturn(jsonRpcRequest);

// Act
routes.invokeJSONRPCHandler(jsonRpcRequest, mockRoutingContext);

// Assert
verify(mockHttpResponse).putHeader(CONTENT_TYPE, APPLICATION_PROBLEM_JSON);
}

/**
* Helper method to set a field via reflection for testing purposes.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.a2a.transport.rest.handler;

import static io.a2a.common.MediaType.APPLICATION_JSON;
import static io.a2a.common.MediaType.APPLICATION_PROBLEM_JSON;
import static io.a2a.server.util.async.AsyncUtils.createTubeConfig;
import static io.a2a.spec.A2AErrorCodes.JSON_PARSE_ERROR_CODE;

Expand Down Expand Up @@ -72,6 +74,7 @@ public class RestHandler {
private static final Logger log = Logger.getLogger(RestHandler.class.getName());
private static final String TASK_STATE_PREFIX = "TASK_STATE_";


// Fields set by constructor injection cannot be final. We need a noargs constructor for
// Jakarta compatibility, and it seems that making fields set by constructor injection
// final, is not proxyable in all runtimes
Expand Down Expand Up @@ -311,7 +314,7 @@ public HTTPRestResponse deleteTaskPushNotificationConfiguration(ServerCallContex
}
DeleteTaskPushNotificationConfigParams params = new DeleteTaskPushNotificationConfigParams(taskId, configId, tenant);
requestHandler.onDeleteTaskPushNotificationConfig(params, context);
return new HTTPRestResponse(204, "application/json", "");
return new HTTPRestResponse(204, APPLICATION_JSON, "");
} catch (A2AError e) {
return createErrorResponse(e);
} catch (Throwable throwable) {
Expand Down Expand Up @@ -344,8 +347,8 @@ private void validate(String json) {
private HTTPRestResponse createSuccessResponse(int statusCode, com.google.protobuf.Message.Builder builder) {
try {
// Include default value fields to ensure empty arrays, zeros, etc. are present in JSON
String jsonBody = JsonFormat.printer().includingDefaultValueFields().print(builder);
return new HTTPRestResponse(statusCode, "application/json", jsonBody);
String jsonBody = JsonFormat.printer().alwaysPrintFieldsWithNoPresence().print(builder);
return new HTTPRestResponse(statusCode, APPLICATION_JSON, jsonBody);
} catch (InvalidProtocolBufferException e) {
return createErrorResponse(new InternalError("Failed to serialize response: " + e.getMessage()));
}
Expand All @@ -358,7 +361,7 @@ public HTTPRestResponse createErrorResponse(A2AError error) {

private HTTPRestResponse createErrorResponse(int statusCode, A2AError error) {
String jsonBody = new HTTPRestErrorResponse(error).toJson();
return new HTTPRestResponse(statusCode, "application/json", jsonBody);
return new HTTPRestResponse(statusCode, APPLICATION_PROBLEM_JSON, jsonBody);
}

private HTTPRestStreamingResponse createStreamingResponse(Flow.Publisher<StreamingEventKind> publisher) {
Expand Down Expand Up @@ -467,7 +470,7 @@ public HTTPRestResponse getExtendedAgentCard(ServerCallContext context, String t
if (!agentCard.capabilities().extendedAgentCard() || extendedAgentCard == null || !extendedAgentCard.isResolvable()) {
throw new ExtendedAgentCardNotConfiguredError(null, "Extended Card not configured", null);
}
return new HTTPRestResponse(200, "application/json", JsonUtil.toJson(extendedAgentCard.get()));
return new HTTPRestResponse(200, APPLICATION_JSON, JsonUtil.toJson(extendedAgentCard.get()));
} catch (A2AError e) {
return createErrorResponse(e);
} catch (Throwable t) {
Expand All @@ -477,7 +480,7 @@ public HTTPRestResponse getExtendedAgentCard(ServerCallContext context, String t

public HTTPRestResponse getAgentCard() {
try {
return new HTTPRestResponse(200, "application/json", JsonUtil.toJson(agentCard));
return new HTTPRestResponse(200, APPLICATION_JSON, JsonUtil.toJson(agentCard));
} catch (Throwable t) {
return createErrorResponse(500, new InternalError(t.getMessage()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public void testGetTaskNotFound() {
RestHandler.HTTPRestResponse response = handler.getTask(callContext, "", "nonexistent", 0);

Assertions.assertEquals(404, response.getStatusCode());
Assertions.assertEquals("application/json", response.getContentType());
Assertions.assertEquals("application/problem+json", response.getContentType());
Assertions.assertTrue(response.getBody().contains("TaskNotFoundError"));
}

Expand All @@ -79,7 +79,7 @@ public void testListTasksInvalidStatus() {
null, null, null);

Assertions.assertEquals(422, response.getStatusCode());
Assertions.assertEquals("application/json", response.getContentType());
Assertions.assertEquals("application/problem+json", response.getContentType());
Assertions.assertTrue(response.getBody().contains("InvalidParamsError"));
}

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

Assertions.assertEquals(400, response.getStatusCode());
Assertions.assertEquals("application/json", response.getContentType());
Assertions.assertEquals("application/problem+json", response.getContentType());
Assertions.assertTrue(response.getBody().contains("JSONParseError"),response.getBody());
}

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

Assertions.assertEquals(422, response.getStatusCode());
Assertions.assertEquals("application/json", response.getContentType());
Assertions.assertEquals("application/problem+json", response.getContentType());
Assertions.assertTrue(response.getBody().contains("InvalidParamsError"));
}

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

Assertions.assertEquals(400, response.getStatusCode());
Assertions.assertEquals("application/json", response.getContentType());
Assertions.assertEquals("application/problem+json", response.getContentType());
Assertions.assertTrue(response.getBody().contains("InvalidRequestError"));
}

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

Assertions.assertEquals(404, response.getStatusCode());
Assertions.assertEquals("application/json", response.getContentType());
Assertions.assertEquals("application/problem+json", response.getContentType());
Assertions.assertTrue(response.getBody().contains("TaskNotFoundError"));
}

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

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

Assertions.assertEquals(501, response.getStatusCode());
Assertions.assertEquals("application/json", response.getContentType());
Assertions.assertEquals("application/problem+json", response.getContentType());
Assertions.assertTrue(response.getBody().contains("VersionNotSupportedError"));
Assertions.assertTrue(response.getBody().contains("2.0"));
}
Expand Down Expand Up @@ -969,7 +969,7 @@ public void testListTasksNegativeTimestampReturns422() {
null, "-1", null);

Assertions.assertEquals(422, response.getStatusCode());
Assertions.assertEquals("application/json", response.getContentType());
Assertions.assertEquals("application/problem+json", response.getContentType());
Assertions.assertTrue(response.getBody().contains("InvalidParamsError"));
}

Expand Down