Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ public class JsonMessages {
"error": {
"code": -32702,
"message": "Invalid parameters",
"details": {"info": "Hello world"}
"data": {"info": "Hello world"}
}
}""";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ public class JsonStreamingMessages {
"error": {
"code": -32602,
"message": "Invalid parameters",
"details": {"info": "Missing required field"}
"data": {"info": "Missing required field"}
}
}""";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -446,9 +446,9 @@ public void testDeleteTaskPushNotificationConfiguration_MethodNameSetInContext()
public void testSendMessage_UnsupportedContentType_ReturnsContentTypeNotSupportedError() {
// Arrange
HTTPRestResponse mockErrorResponse = mock(HTTPRestResponse.class);
when(mockErrorResponse.getStatusCode()).thenReturn(415);
when(mockErrorResponse.getStatusCode()).thenReturn(400);
when(mockErrorResponse.getContentType()).thenReturn(APPLICATION_JSON);
when(mockErrorResponse.getBody()).thenReturn("{\"error\":{\"code\":415,\"status\":\"INVALID_ARGUMENT\",\"message\":\"Incompatible content types\",\"details\":[{\"reason\":\"CONTENT_TYPE_NOT_SUPPORTED\",\"domain\":\"a2a-protocol.org\"}]}}");
when(mockErrorResponse.getBody()).thenReturn("{\"error\":{\"code\":400,\"status\":\"INVALID_ARGUMENT\",\"message\":\"Incompatible content types\",\"details\":[{\"reason\":\"CONTENT_TYPE_NOT_SUPPORTED\",\"domain\":\"a2a-protocol.org\"}]}}");
when(mockRestHandler.createErrorResponse(any(ContentTypeNotSupportedError.class))).thenReturn(mockErrorResponse);
when(mockRequest.getHeader(any(CharSequence.class))).thenReturn("text/plain");

Expand All @@ -464,9 +464,9 @@ public void testSendMessage_UnsupportedContentType_ReturnsContentTypeNotSupporte
public void testSendMessageStreaming_UnsupportedContentType_ReturnsContentTypeNotSupportedError() {
// Arrange
HTTPRestResponse mockErrorResponse = mock(HTTPRestResponse.class);
when(mockErrorResponse.getStatusCode()).thenReturn(415);
when(mockErrorResponse.getStatusCode()).thenReturn(400);
when(mockErrorResponse.getContentType()).thenReturn(APPLICATION_JSON);
when(mockErrorResponse.getBody()).thenReturn("{\"error\":{\"code\":415,\"status\":\"INVALID_ARGUMENT\",\"message\":\"Incompatible content types\",\"details\":[{\"reason\":\"CONTENT_TYPE_NOT_SUPPORTED\",\"domain\":\"a2a-protocol.org\"}]}}");
when(mockErrorResponse.getBody()).thenReturn("{\"error\":{\"code\":400,\"status\":\"INVALID_ARGUMENT\",\"message\":\"Incompatible content types\",\"details\":[{\"reason\":\"CONTENT_TYPE_NOT_SUPPORTED\",\"domain\":\"a2a-protocol.org\"}]}}");
when(mockRestHandler.createErrorResponse(any(ContentTypeNotSupportedError.class))).thenReturn(mockErrorResponse);
when(mockRequest.getHeader(any(CharSequence.class))).thenReturn("text/plain");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public void testSendMessageWithUnsupportedContentType() throws Exception {
.header("Content-Type", "text/plain")
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
Assertions.assertEquals(415, response.statusCode());
Assertions.assertEquals(400, response.statusCode());
Assertions.assertTrue(response.body().contains("CONTENT_TYPE_NOT_SUPPORTED"),
"Expected CONTENT_TYPE_NOT_SUPPORTED in response body: " + response.body());
}
Expand All @@ -58,7 +58,7 @@ public void testSendMessageWithUnsupportedProtocolVersion() throws Exception {
.header("A2A-Version", "0.4.0")
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
Assertions.assertEquals(400, response.statusCode());
Assertions.assertEquals(501, response.statusCode());
Assertions.assertTrue(response.body().contains("VERSION_NOT_SUPPORTED"),
"Expected VERSION_NOT_SUPPORTED in response body: " + response.body());
}
Expand Down
6 changes: 3 additions & 3 deletions spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -392,8 +392,8 @@ private static A2AError processError(JsonObject error) {
String message = error.has("message") ? error.get("message").getAsString() : null;
Integer code = error.has("code") ? error.get("code").getAsInt() : null;
Map<String, Object> details = null;
if (error.has("details") && error.get("details").isJsonObject()) {
details =GSON.fromJson(error.get("details"), Map.class);
if (error.has("data") && error.get("data").isJsonObject()) {
details = GSON.fromJson(error.get("data"), Map.class);
}
if (code != null) {
A2AErrorCodes errorCode = A2AErrorCodes.fromCode(code);
Expand Down Expand Up @@ -606,7 +606,7 @@ public static String toJsonRPCErrorResponse(Object requestId, A2AError error) {
output.name("code").value(error.getCode());
output.name("message").value(error.getMessage());
if (!error.getDetails().isEmpty()) {
output.name("details");
output.name("data");
GSON.toJson(error.getDetails(), Map.class, output);
}
output.endObject();
Expand Down
12 changes: 6 additions & 6 deletions spec/src/main/java/io/a2a/spec/A2AErrorCodes.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,19 @@ public enum A2AErrorCodes {
TASK_NOT_FOUND(-32001, "NOT_FOUND", 404),

/** Error code indicating the task cannot be canceled in its current state (-32002). */
TASK_NOT_CANCELABLE(-32002, "FAILED_PRECONDITION", 409),
TASK_NOT_CANCELABLE(-32002, "FAILED_PRECONDITION", 400),

/** Error code indicating push notifications are not supported by this agent (-32003). */
PUSH_NOTIFICATION_NOT_SUPPORTED(-32003, "UNIMPLEMENTED", 400),
PUSH_NOTIFICATION_NOT_SUPPORTED(-32003, "FAILED_PRECONDITION", 400),

/** Error code indicating the requested operation is not supported (-32004). */
UNSUPPORTED_OPERATION(-32004, "UNIMPLEMENTED", 400),
UNSUPPORTED_OPERATION(-32004, "UNIMPLEMENTED", 501),

/** Error code indicating the content type is not supported (-32005). */
CONTENT_TYPE_NOT_SUPPORTED(-32005, "INVALID_ARGUMENT", 415),
CONTENT_TYPE_NOT_SUPPORTED(-32005, "INVALID_ARGUMENT", 400),

/** Error code indicating the agent returned an invalid response (-32006). */
INVALID_AGENT_RESPONSE(-32006, "INTERNAL", 502),
INVALID_AGENT_RESPONSE(-32006, "INTERNAL", 500),

/** Error code indicating extended agent card is not configured (-32007). */
EXTENDED_AGENT_CARD_NOT_CONFIGURED(-32007, "FAILED_PRECONDITION", 400),
Expand All @@ -43,7 +43,7 @@ public enum A2AErrorCodes {

/** Error code indicating the A2A protocol version specified in the request (via A2A-Version service parameter)
* is not supported by the agent (-32009). */
VERSION_NOT_SUPPORTED(-32009, "UNIMPLEMENTED", 400),
VERSION_NOT_SUPPORTED(-32009, "UNIMPLEMENTED", 501),

/** JSON-RPC error code for invalid request structure (-32600). */
INVALID_REQUEST(-32600, "INVALID_ARGUMENT", 400),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -706,7 +706,7 @@ private <V> ServerCallContext createCallContext(StreamObserver<V> responseObserv
* <li>{@link InternalError} → {@code INTERNAL}</li>
* <li>{@link TaskNotFoundError} → {@code NOT_FOUND}</li>
* <li>{@link TaskNotCancelableError} → {@code FAILED_PRECONDITION}</li>
* <li>{@link PushNotificationNotSupportedError} → {@code UNIMPLEMENTED}</li>
* <li>{@link PushNotificationNotSupportedError} → {@code FAILED_PRECONDITION}</li>
* <li>{@link UnsupportedOperationError} → {@code UNIMPLEMENTED}</li>
* <li>{@link JSONParseError} → {@code INTERNAL}</li>
* <li>{@link ContentTypeNotSupportedError} → {@code INVALID_ARGUMENT}</li>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ public void testPushNotificationsNotSupportedError() throws Exception {
GrpcHandler handler = new TestGrpcHandler(card, requestHandler, internalExecutor);
StreamRecorder<TaskPushNotificationConfig> streamRecorder = createTaskPushNotificationConfigRequest(handler,
AbstractA2ARequestHandlerTest.MINIMAL_TASK.id(), AbstractA2ARequestHandlerTest.MINIMAL_TASK.id());
assertGrpcError(streamRecorder, Status.Code.UNIMPLEMENTED);
assertGrpcError(streamRecorder, Status.Code.FAILED_PRECONDITION);
}

@Test
Expand Down Expand Up @@ -656,7 +656,7 @@ public void testListPushNotificationConfigNotSupported() throws Exception {
.build();
StreamRecorder<ListTaskPushNotificationConfigsResponse> streamRecorder = StreamRecorder.create();
handler.listTaskPushNotificationConfigs(request, streamRecorder);
assertGrpcError(streamRecorder, Status.Code.UNIMPLEMENTED);
assertGrpcError(streamRecorder, Status.Code.FAILED_PRECONDITION);
}

@Test
Expand Down Expand Up @@ -727,7 +727,7 @@ public void testDeletePushNotificationConfigNotSupported() throws Exception {
.build();
StreamRecorder<Empty> streamRecorder = StreamRecorder.create();
handler.deleteTaskPushNotificationConfig(request, streamRecorder);
assertGrpcError(streamRecorder, Status.Code.UNIMPLEMENTED);
assertGrpcError(streamRecorder, Status.Code.FAILED_PRECONDITION);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import static io.a2a.common.MediaType.APPLICATION_JSON;
import static io.a2a.server.util.async.AsyncUtils.createTubeConfig;

import io.a2a.spec.A2AErrorCodes;

import java.time.Instant;
import java.time.format.DateTimeParseException;
Expand Down Expand Up @@ -40,6 +39,7 @@
import io.a2a.server.version.A2AVersionValidator;
import io.a2a.server.util.async.Internal;
import io.a2a.spec.A2AError;
import io.a2a.spec.A2AErrorCodes;
import io.a2a.spec.AgentCard;
import io.a2a.spec.CancelTaskParams;
import io.a2a.spec.ContentTypeNotSupportedError;
Expand Down Expand Up @@ -122,13 +122,13 @@
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
private AgentCard agentCard;
private @Nullable Instance<AgentCard> extendedAgentCard;
private @Nullable
Instance<AgentCard> extendedAgentCard;
Comment on lines +130 to +131
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The @Nullable annotation should be on the same line as the field type to maintain consistent formatting and readability.

Suggested change
private @Nullable
Instance<AgentCard> extendedAgentCard;
private @Nullable Instance<AgentCard> extendedAgentCard;

private AgentCardCacheMetadata cacheMetadata;
private RequestHandler requestHandler;
private Executor executor;
Expand Down Expand Up @@ -377,7 +377,7 @@ public HTTPRestResponse createTaskPushNotificationConfiguration(ServerCallContex
if (!taskIdFromBody.isEmpty() && !taskIdFromBody.equals(taskId)) {
throw new InvalidParamsError("Task ID in request body (" + taskIdFromBody + ") does not match task ID in URL path (" + taskId + ").");
}

builder.setTenant(tenant);
builder.setTaskId(taskId);
TaskPushNotificationConfig result = requestHandler.onCreateTaskPushNotificationConfig(ProtoUtils.FromProto.createTaskPushNotificationConfig(builder), context);
Expand Down Expand Up @@ -766,29 +766,38 @@ private static int mapErrorToHttpStatus(A2AError error) {
if (error instanceof InvalidParamsError) {
return 422;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For consistency with the rest of the method and to ensure that HTTP status codes are centralized in the A2AErrorCodes enum, please use A2AErrorCodes.INVALID_PARAMS.httpCode() instead of a hardcoded literal. This ensures adherence to the A2A protocol specification.

Suggested change
return 422;
return A2AErrorCodes.INVALID_PARAMS.httpCode();
References
  1. Adhere to the A2A protocol specification for error code mappings and move constants used across multiple modules to a shared location to ensure consistency.

}
if (error instanceof MethodNotFoundError || error instanceof TaskNotFoundError) {
return 404;
if (error instanceof MethodNotFoundError) {
return A2AErrorCodes.METHOD_NOT_FOUND.httpCode();
}
if (error instanceof TaskNotFoundError) {
return A2AErrorCodes.TASK_NOT_FOUND.httpCode();
}
if (error instanceof TaskNotCancelableError) {
return 409;
return A2AErrorCodes.TASK_NOT_CANCELABLE.httpCode();
}
if (error instanceof UnsupportedOperationError) {
return 501;
return A2AErrorCodes.UNSUPPORTED_OPERATION.httpCode();
}
if (error instanceof ContentTypeNotSupportedError) {
return 415;
return A2AErrorCodes.CONTENT_TYPE_NOT_SUPPORTED.httpCode();
}
if (error instanceof InvalidAgentResponseError) {
return 502;
return A2AErrorCodes.INVALID_AGENT_RESPONSE.httpCode();
}
if (error instanceof ExtendedAgentCardNotConfiguredError) {
return A2AErrorCodes.EXTENDED_AGENT_CARD_NOT_CONFIGURED.httpCode();
}
if (error instanceof ExtensionSupportRequiredError) {
return A2AErrorCodes.EXTENSION_SUPPORT_REQUIRED.httpCode();
}
if (error instanceof ExtendedAgentCardNotConfiguredError
|| error instanceof ExtensionSupportRequiredError
|| error instanceof VersionNotSupportedError
|| error instanceof PushNotificationNotSupportedError) {
return 400;
if (error instanceof VersionNotSupportedError) {
return A2AErrorCodes.VERSION_NOT_SUPPORTED.httpCode();
}
if (error instanceof PushNotificationNotSupportedError) {
return A2AErrorCodes.PUSH_NOTIFICATION_NOT_SUPPORTED.httpCode();
}
if (error instanceof InternalError) {
return 500;
return A2AErrorCodes.INTERNAL.httpCode();
}
return 500;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

It is better to use the centralized A2AErrorCodes.INTERNAL.httpCode() as the default return value to ensure consistency across the transport layer and adherence to the A2A protocol specification.

Suggested change
return 500;
return A2AErrorCodes.INTERNAL.httpCode();
References
  1. Adhere to the A2A protocol specification for error code mappings and move constants used across multiple modules to a shared location to ensure consistency.

}
Expand Down Expand Up @@ -827,7 +836,7 @@ public HTTPRestResponse getExtendedAgentCard(ServerCallContext context, String t
} catch (A2AError e) {
return createErrorResponse(e);
} catch (Throwable t) {
return createErrorResponse(500, new InternalError(t.getMessage()));
return createErrorResponse(A2AErrorCodes.INTERNAL.httpCode(), new InternalError(t.getMessage()));
}
}

Expand Down Expand Up @@ -1036,12 +1045,16 @@ public String toString() {
return "HTTPRestErrorResponse{error=" + error + '}';
}

private record ErrorBody(int code, String status, String message, List<ErrorDetail> details) {}
private record ErrorBody(int code, String status, String message, List<ErrorDetail> details) {

}

private record ErrorDetail(
@com.google.gson.annotations.SerializedName("@type") String type,
String reason,
String domain,
Map<String, Object> metadata) {}
Map<String, Object> metadata) {

}
Comment on lines +1048 to +1058
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These records were previously defined as concise one-liners. The addition of empty lines inside the record bodies is unnecessary and deviates from the project's style for simple data carriers.

        private record ErrorBody(int code, String status, String message, List<ErrorDetail> details) {}

        private record ErrorDetail(
                @com.google.gson.annotations.SerializedName("@type") String type,
                String reason,
                String domain,
                Map<String, Object> metadata) {}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -798,7 +798,7 @@ public void testVersionNotSupportedErrorOnSendMessage() {

RestHandler.HTTPRestResponse response = handler.sendMessage(contextWithVersion, "", requestBody);

assertProblemDetail(response, 400,
assertProblemDetail(response, 501,
"VERSION_NOT_SUPPORTED",
"Protocol version '2.0' is not supported. Supported versions: [1.0]");
}
Expand Down
Loading