diff --git a/client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/GrpcErrorMapper.java b/client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/GrpcErrorMapper.java
index b9ad3325b..781242426 100644
--- a/client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/GrpcErrorMapper.java
+++ b/client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/GrpcErrorMapper.java
@@ -1,7 +1,13 @@
package io.a2a.client.transport.grpc;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import org.jspecify.annotations.Nullable;
import io.a2a.common.A2AErrorMessages;
import io.a2a.spec.A2AClientException;
+import io.a2a.spec.A2AErrorCodes;
import io.a2a.spec.ContentTypeNotSupportedError;
import io.a2a.spec.ExtendedAgentCardNotConfiguredError;
import io.a2a.spec.ExtensionSupportRequiredError;
@@ -16,12 +22,33 @@
import io.a2a.spec.UnsupportedOperationError;
import io.a2a.spec.VersionNotSupportedError;
import io.grpc.Status;
+import io.grpc.protobuf.StatusProto;
/**
- * Utility class to map gRPC exceptions to appropriate A2A error types
+ * Utility class to map gRPC exceptions to appropriate A2A error types.
+ *
+ * Extracts {@code google.rpc.ErrorInfo} from gRPC status details to identify the
+ * specific A2A error type via the {@code reason} field.
*/
public class GrpcErrorMapper {
+ private static final Map REASON_MAP = Map.ofEntries(
+ Map.entry("TASK_NOT_FOUND", A2AErrorCodes.TASK_NOT_FOUND),
+ Map.entry("TASK_NOT_CANCELABLE", A2AErrorCodes.TASK_NOT_CANCELABLE),
+ Map.entry("PUSH_NOTIFICATION_NOT_SUPPORTED", A2AErrorCodes.PUSH_NOTIFICATION_NOT_SUPPORTED),
+ Map.entry("UNSUPPORTED_OPERATION", A2AErrorCodes.UNSUPPORTED_OPERATION),
+ Map.entry("CONTENT_TYPE_NOT_SUPPORTED", A2AErrorCodes.CONTENT_TYPE_NOT_SUPPORTED),
+ Map.entry("INVALID_AGENT_RESPONSE", A2AErrorCodes.INVALID_AGENT_RESPONSE),
+ Map.entry("EXTENDED_AGENT_CARD_NOT_CONFIGURED", A2AErrorCodes.EXTENDED_AGENT_CARD_NOT_CONFIGURED),
+ Map.entry("EXTENSION_SUPPORT_REQUIRED", A2AErrorCodes.EXTENSION_SUPPORT_REQUIRED),
+ Map.entry("VERSION_NOT_SUPPORTED", A2AErrorCodes.VERSION_NOT_SUPPORTED),
+ Map.entry("INVALID_REQUEST", A2AErrorCodes.INVALID_REQUEST),
+ Map.entry("METHOD_NOT_FOUND", A2AErrorCodes.METHOD_NOT_FOUND),
+ Map.entry("INVALID_PARAMS", A2AErrorCodes.INVALID_PARAMS),
+ Map.entry("INTERNAL", A2AErrorCodes.INTERNAL),
+ Map.entry("JSON_PARSE", A2AErrorCodes.JSON_PARSE)
+ );
+
public static A2AClientException mapGrpcError(Throwable e) {
return mapGrpcError(e, "gRPC error: ");
}
@@ -29,57 +56,68 @@ public static A2AClientException mapGrpcError(Throwable e) {
public static A2AClientException mapGrpcError(Throwable e, String errorPrefix) {
Status status = Status.fromThrowable(e);
Status.Code code = status.getCode();
- String description = status.getDescription();
-
- // Extract the actual error type from the description if possible
- // (using description because the same code can map to multiple errors -
- // see GrpcHandler#handleError)
- if (description != null) {
- if (description.contains("TaskNotFoundError")) {
- return new A2AClientException(errorPrefix + description, new TaskNotFoundError());
- } else if (description.contains("UnsupportedOperationError")) {
- return new A2AClientException(errorPrefix + description, new UnsupportedOperationError());
- } else if (description.contains("InvalidParamsError")) {
- return new A2AClientException(errorPrefix + description, new InvalidParamsError());
- } else if (description.contains("InvalidRequestError")) {
- return new A2AClientException(errorPrefix + description, new InvalidRequestError());
- } else if (description.contains("MethodNotFoundError")) {
- return new A2AClientException(errorPrefix + description, new MethodNotFoundError());
- } else if (description.contains("TaskNotCancelableError")) {
- return new A2AClientException(errorPrefix + description, new TaskNotCancelableError());
- } else if (description.contains("PushNotificationNotSupportedError")) {
- return new A2AClientException(errorPrefix + description, new PushNotificationNotSupportedError());
- } else if (description.contains("JSONParseError")) {
- return new A2AClientException(errorPrefix + description, new JSONParseError());
- } else if (description.contains("ContentTypeNotSupportedError")) {
- return new A2AClientException(errorPrefix + description, new ContentTypeNotSupportedError(null, description, null));
- } else if (description.contains("InvalidAgentResponseError")) {
- return new A2AClientException(errorPrefix + description, new InvalidAgentResponseError(null, description, null));
- } else if (description.contains("ExtendedCardNotConfiguredError")) {
- return new A2AClientException(errorPrefix + description, new ExtendedAgentCardNotConfiguredError(null, description, null));
- } else if (description.contains("ExtensionSupportRequiredError")) {
- return new A2AClientException(errorPrefix + description, new ExtensionSupportRequiredError(null, description, null));
- } else if (description.contains("VersionNotSupportedError")) {
- return new A2AClientException(errorPrefix + description, new VersionNotSupportedError(null, description, null));
+ String message = status.getDescription();
+
+ // Try to extract ErrorInfo from status details
+ com.google.rpc.@Nullable ErrorInfo errorInfo = extractErrorInfo(e);
+ if (errorInfo != null) {
+ A2AErrorCodes errorCode = REASON_MAP.get(errorInfo.getReason());
+ if (errorCode != null) {
+ String errorMessage = message != null ? message : (e.getMessage() != null ? e.getMessage() : "");
+ Map metadata = errorInfo.getMetadataMap().isEmpty() ? null
+ : new HashMap(errorInfo.getMetadataMap());
+ return mapByErrorCode(errorCode, errorPrefix + errorMessage, errorMessage, metadata);
}
}
-
+
// Fall back to mapping based on status code
- switch (code) {
- case NOT_FOUND:
- return new A2AClientException(errorPrefix + (description != null ? description : e.getMessage()), new TaskNotFoundError());
- case UNIMPLEMENTED:
- return new A2AClientException(errorPrefix + (description != null ? description : e.getMessage()), new UnsupportedOperationError());
- case INVALID_ARGUMENT:
- return new A2AClientException(errorPrefix + (description != null ? description : e.getMessage()), new InvalidParamsError());
- case INTERNAL:
- return new A2AClientException(errorPrefix + (description != null ? description : e.getMessage()), new io.a2a.spec.InternalError(null, e.getMessage(), null));
- case UNAUTHENTICATED:
- return new A2AClientException(errorPrefix + A2AErrorMessages.AUTHENTICATION_FAILED);
- case PERMISSION_DENIED:
- return new A2AClientException(errorPrefix + A2AErrorMessages.AUTHORIZATION_FAILED);
- default:
- return new A2AClientException(errorPrefix + e.getMessage(), e);
+ String desc = message != null ? message : e.getMessage() == null ? "" : e.getMessage();
+ return switch (code) {
+ case NOT_FOUND -> new A2AClientException(errorPrefix + desc, new TaskNotFoundError());
+ case UNIMPLEMENTED -> new A2AClientException(errorPrefix + desc, new UnsupportedOperationError());
+ case INVALID_ARGUMENT -> new A2AClientException(errorPrefix + desc, new InvalidParamsError());
+ case INTERNAL -> new A2AClientException(errorPrefix + desc, new io.a2a.spec.InternalError(null, desc, null));
+ case UNAUTHENTICATED -> new A2AClientException(errorPrefix + A2AErrorMessages.AUTHENTICATION_FAILED);
+ case PERMISSION_DENIED -> new A2AClientException(errorPrefix + A2AErrorMessages.AUTHORIZATION_FAILED);
+ default -> new A2AClientException(errorPrefix + e.getMessage(), e);
+ };
+ }
+
+ private static com.google.rpc.@Nullable ErrorInfo extractErrorInfo(Throwable e) {
+ try {
+ com.google.rpc.Status rpcStatus = StatusProto.fromThrowable(e);
+ if (rpcStatus != null) {
+ for (com.google.protobuf.Any detail : rpcStatus.getDetailsList()) {
+ if (detail.is(com.google.rpc.ErrorInfo.class)) {
+ com.google.rpc.ErrorInfo errorInfo = detail.unpack(com.google.rpc.ErrorInfo.class);
+ if ("a2a-protocol.org".equals(errorInfo.getDomain())) {
+ return errorInfo;
+ }
+ }
+ }
+ }
+ } catch (InvalidProtocolBufferException ignored) {
+ // Fall through to status code-based mapping
}
+ return null;
+ }
+
+ private static A2AClientException mapByErrorCode(A2AErrorCodes errorCode, String fullMessage, String errorMessage, @Nullable Map metadata) {
+ return switch (errorCode) {
+ case TASK_NOT_FOUND -> new A2AClientException(fullMessage, new TaskNotFoundError(errorMessage, metadata));
+ case TASK_NOT_CANCELABLE -> new A2AClientException(fullMessage, new TaskNotCancelableError(null, errorMessage, metadata));
+ case PUSH_NOTIFICATION_NOT_SUPPORTED -> new A2AClientException(fullMessage, new PushNotificationNotSupportedError(null, errorMessage, metadata));
+ case UNSUPPORTED_OPERATION -> new A2AClientException(fullMessage, new UnsupportedOperationError(null, errorMessage, metadata));
+ case CONTENT_TYPE_NOT_SUPPORTED -> new A2AClientException(fullMessage, new ContentTypeNotSupportedError(null, errorMessage, metadata));
+ case INVALID_AGENT_RESPONSE -> new A2AClientException(fullMessage, new InvalidAgentResponseError(null, errorMessage, metadata));
+ case EXTENDED_AGENT_CARD_NOT_CONFIGURED -> new A2AClientException(fullMessage, new ExtendedAgentCardNotConfiguredError(null, errorMessage, metadata));
+ case EXTENSION_SUPPORT_REQUIRED -> new A2AClientException(fullMessage, new ExtensionSupportRequiredError(null, errorMessage, metadata));
+ case VERSION_NOT_SUPPORTED -> new A2AClientException(fullMessage, new VersionNotSupportedError(null, errorMessage, metadata));
+ case INVALID_REQUEST -> new A2AClientException(fullMessage, new InvalidRequestError(null, errorMessage, metadata));
+ case JSON_PARSE -> new A2AClientException(fullMessage, new JSONParseError(null, errorMessage, metadata));
+ case METHOD_NOT_FOUND -> new A2AClientException(fullMessage, new MethodNotFoundError(null, errorMessage, metadata));
+ case INVALID_PARAMS -> new A2AClientException(fullMessage, new InvalidParamsError(null, errorMessage, metadata));
+ case INTERNAL -> new A2AClientException(fullMessage, new io.a2a.spec.InternalError(null, errorMessage, metadata));
+ };
}
}
diff --git a/client/transport/grpc/src/test/java/io/a2a/client/transport/grpc/GrpcErrorMapperTest.java b/client/transport/grpc/src/test/java/io/a2a/client/transport/grpc/GrpcErrorMapperTest.java
index 864e406d2..151ca7b91 100644
--- a/client/transport/grpc/src/test/java/io/a2a/client/transport/grpc/GrpcErrorMapperTest.java
+++ b/client/transport/grpc/src/test/java/io/a2a/client/transport/grpc/GrpcErrorMapperTest.java
@@ -1,10 +1,11 @@
package io.a2a.client.transport.grpc;
-import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import com.google.protobuf.Any;
+import com.google.rpc.ErrorInfo;
import io.a2a.spec.A2AClientException;
import io.a2a.spec.ContentTypeNotSupportedError;
import io.a2a.spec.ExtendedAgentCardNotConfiguredError;
@@ -15,25 +16,38 @@
import io.a2a.spec.VersionNotSupportedError;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
+import io.grpc.protobuf.StatusProto;
import org.junit.jupiter.api.Test;
/**
* Tests for GrpcErrorMapper - verifies correct unmarshalling of gRPC errors to A2A error types
+ * using google.rpc.ErrorInfo in status details.
*/
public class GrpcErrorMapperTest {
+ private static StatusRuntimeException createA2AStatusException(int grpcCode, String message, String reason) {
+ ErrorInfo errorInfo = ErrorInfo.newBuilder()
+ .setReason(reason)
+ .setDomain("a2a-protocol.org")
+ .build();
+
+ com.google.rpc.Status rpcStatus = com.google.rpc.Status.newBuilder()
+ .setCode(grpcCode)
+ .setMessage(message)
+ .addDetails(Any.pack(errorInfo))
+ .build();
+
+ return StatusProto.toStatusRuntimeException(rpcStatus);
+ }
+
@Test
public void testExtensionSupportRequiredErrorUnmarshalling() {
- // Create a gRPC StatusRuntimeException with ExtensionSupportRequiredError in description
- String errorMessage = "ExtensionSupportRequiredError: Extension required: https://example.com/test-extension";
- StatusRuntimeException grpcException = Status.FAILED_PRECONDITION
- .withDescription(errorMessage)
- .asRuntimeException();
+ String errorMessage = "Extension required: https://example.com/test-extension";
+ StatusRuntimeException grpcException = createA2AStatusException(
+ Status.Code.FAILED_PRECONDITION.value(), errorMessage, "EXTENSION_SUPPORT_REQUIRED");
- // Map the gRPC error to A2A error
A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
- // Verify the result
assertNotNull(result);
assertNotNull(result.getCause());
assertInstanceOf(ExtensionSupportRequiredError.class, result.getCause());
@@ -46,16 +60,12 @@ public void testExtensionSupportRequiredErrorUnmarshalling() {
@Test
public void testVersionNotSupportedErrorUnmarshalling() {
- // Create a gRPC StatusRuntimeException with VersionNotSupportedError in description
- String errorMessage = "VersionNotSupportedError: Version 2.0 is not supported";
- StatusRuntimeException grpcException = Status.FAILED_PRECONDITION
- .withDescription(errorMessage)
- .asRuntimeException();
+ String errorMessage = "Version 2.0 is not supported";
+ StatusRuntimeException grpcException = createA2AStatusException(
+ Status.Code.UNIMPLEMENTED.value(), errorMessage, "VERSION_NOT_SUPPORTED");
- // Map the gRPC error to A2A error
A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
- // Verify the result
assertNotNull(result);
assertNotNull(result.getCause());
assertInstanceOf(VersionNotSupportedError.class, result.getCause());
@@ -67,16 +77,12 @@ public void testVersionNotSupportedErrorUnmarshalling() {
@Test
public void testExtendedCardNotConfiguredErrorUnmarshalling() {
- // Create a gRPC StatusRuntimeException with ExtendedCardNotConfiguredError in description
- String errorMessage = "ExtendedCardNotConfiguredError: Extended card not configured for this agent";
- StatusRuntimeException grpcException = Status.FAILED_PRECONDITION
- .withDescription(errorMessage)
- .asRuntimeException();
+ String errorMessage = "Extended card not configured for this agent";
+ StatusRuntimeException grpcException = createA2AStatusException(
+ Status.Code.FAILED_PRECONDITION.value(), errorMessage, "EXTENDED_AGENT_CARD_NOT_CONFIGURED");
- // Map the gRPC error to A2A error
A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
- // Verify the result
assertNotNull(result);
assertNotNull(result.getCause());
assertInstanceOf(ExtendedAgentCardNotConfiguredError.class, result.getCause());
@@ -88,16 +94,12 @@ public void testExtendedCardNotConfiguredErrorUnmarshalling() {
@Test
public void testTaskNotFoundErrorUnmarshalling() {
- // Create a gRPC StatusRuntimeException with TaskNotFoundError in description
- String errorMessage = "TaskNotFoundError: Task task-123 not found";
- StatusRuntimeException grpcException = Status.NOT_FOUND
- .withDescription(errorMessage)
- .asRuntimeException();
+ String errorMessage = "Task task-123 not found";
+ StatusRuntimeException grpcException = createA2AStatusException(
+ Status.Code.NOT_FOUND.value(), errorMessage, "TASK_NOT_FOUND");
- // Map the gRPC error to A2A error
A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
- // Verify the result
assertNotNull(result);
assertNotNull(result.getCause());
assertInstanceOf(TaskNotFoundError.class, result.getCause());
@@ -105,16 +107,12 @@ public void testTaskNotFoundErrorUnmarshalling() {
@Test
public void testUnsupportedOperationErrorUnmarshalling() {
- // Create a gRPC StatusRuntimeException with UnsupportedOperationError in description
- String errorMessage = "UnsupportedOperationError: Operation not supported";
- StatusRuntimeException grpcException = Status.UNIMPLEMENTED
- .withDescription(errorMessage)
- .asRuntimeException();
+ String errorMessage = "Operation not supported";
+ StatusRuntimeException grpcException = createA2AStatusException(
+ Status.Code.UNIMPLEMENTED.value(), errorMessage, "UNSUPPORTED_OPERATION");
- // Map the gRPC error to A2A error
A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
- // Verify the result
assertNotNull(result);
assertNotNull(result.getCause());
assertInstanceOf(UnsupportedOperationError.class, result.getCause());
@@ -122,16 +120,12 @@ public void testUnsupportedOperationErrorUnmarshalling() {
@Test
public void testInvalidParamsErrorUnmarshalling() {
- // Create a gRPC StatusRuntimeException with InvalidParamsError in description
- String errorMessage = "InvalidParamsError: Invalid parameters provided";
- StatusRuntimeException grpcException = Status.INVALID_ARGUMENT
- .withDescription(errorMessage)
- .asRuntimeException();
+ String errorMessage = "Invalid parameters provided";
+ StatusRuntimeException grpcException = createA2AStatusException(
+ Status.Code.INVALID_ARGUMENT.value(), errorMessage, "INVALID_PARAMS");
- // Map the gRPC error to A2A error
A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
- // Verify the result
assertNotNull(result);
assertNotNull(result.getCause());
assertInstanceOf(InvalidParamsError.class, result.getCause());
@@ -139,16 +133,12 @@ public void testInvalidParamsErrorUnmarshalling() {
@Test
public void testContentTypeNotSupportedErrorUnmarshalling() {
- // Create a gRPC StatusRuntimeException with ContentTypeNotSupportedError in description
- String errorMessage = "ContentTypeNotSupportedError: Content type application/xml not supported";
- StatusRuntimeException grpcException = Status.FAILED_PRECONDITION
- .withDescription(errorMessage)
- .asRuntimeException();
+ String errorMessage = "Content type application/xml not supported";
+ StatusRuntimeException grpcException = createA2AStatusException(
+ Status.Code.INVALID_ARGUMENT.value(), errorMessage, "CONTENT_TYPE_NOT_SUPPORTED");
- // Map the gRPC error to A2A error
A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
- // Verify the result
assertNotNull(result);
assertNotNull(result.getCause());
assertInstanceOf(ContentTypeNotSupportedError.class, result.getCause());
@@ -160,12 +150,11 @@ public void testContentTypeNotSupportedErrorUnmarshalling() {
@Test
public void testFallbackToStatusCodeMapping() {
- // Create a gRPC StatusRuntimeException without specific error type in description
+ // Create a gRPC StatusRuntimeException without ErrorInfo details
StatusRuntimeException grpcException = Status.NOT_FOUND
.withDescription("Generic not found error")
.asRuntimeException();
- // Map the gRPC error to A2A error
A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
// Verify fallback to status code mapping
@@ -176,17 +165,13 @@ public void testFallbackToStatusCodeMapping() {
@Test
public void testCustomErrorPrefix() {
- // Create a gRPC StatusRuntimeException
- String errorMessage = "ExtensionSupportRequiredError: Extension required: https://example.com/ext";
- StatusRuntimeException grpcException = Status.FAILED_PRECONDITION
- .withDescription(errorMessage)
- .asRuntimeException();
+ String errorMessage = "Extension required: https://example.com/ext";
+ StatusRuntimeException grpcException = createA2AStatusException(
+ Status.Code.FAILED_PRECONDITION.value(), errorMessage, "EXTENSION_SUPPORT_REQUIRED");
- // Map with custom error prefix
String customPrefix = "Custom Error: ";
A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException, customPrefix);
- // Verify custom prefix is used
assertNotNull(result);
assertTrue(result.getMessage().startsWith(customPrefix));
assertInstanceOf(ExtensionSupportRequiredError.class, result.getCause());
diff --git a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransport.java b/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransport.java
index 2bbb25d87..63100ee09 100644
--- a/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransport.java
+++ b/client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransport.java
@@ -364,7 +364,7 @@ private > T unmarshalResponse(String response, String m
A2AResponse> value = JSONRPCUtils.parseResponseBody(response, method);
A2AError error = value.getError();
if (error != null) {
- throw new A2AClientException(error.getMessage() + (error.getData() != null ? ": " + error.getData() : ""), error);
+ throw new A2AClientException(error.getMessage() + (!error.getDetails().isEmpty() ? ": " + error.getDetails() : ""), error);
}
// Safe cast: JSONRPCUtils.parseResponseBody returns the correct concrete type based on method
return (T) value;
diff --git a/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportTest.java b/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportTest.java
index 47c450506..55c20bc55 100644
--- a/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportTest.java
+++ b/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportTest.java
@@ -212,7 +212,7 @@ public void testA2AClientSendMessageWithError() throws Exception {
client.sendMessage(params, null);
fail(); // should not reach here
} catch (A2AClientException e) {
- assertTrue(e.getMessage().contains("Invalid parameters: \"Hello world\""),e.getMessage());
+ assertTrue(e.getMessage().contains("Invalid parameters: {info=Hello world}"),e.getMessage());
}
}
diff --git a/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JsonMessages.java b/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JsonMessages.java
index a9639b84c..a0be30e0d 100644
--- a/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JsonMessages.java
+++ b/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JsonMessages.java
@@ -216,7 +216,7 @@ public class JsonMessages {
"error": {
"code": -32702,
"message": "Invalid parameters",
- "data": "Hello world"
+ "details": {"info": "Hello world"}
}
}""";
diff --git a/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JsonStreamingMessages.java b/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JsonStreamingMessages.java
index 39f2d7859..df6fee000 100644
--- a/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JsonStreamingMessages.java
+++ b/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JsonStreamingMessages.java
@@ -99,7 +99,7 @@ public class JsonStreamingMessages {
"error": {
"code": -32602,
"message": "Invalid parameters",
- "data": "Missing required field"
+ "details": {"info": "Missing required field"}
}
}""";
diff --git a/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListenerTest.java b/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListenerTest.java
index e9e74d12f..389c7df8d 100644
--- a/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListenerTest.java
+++ b/client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListenerTest.java
@@ -161,7 +161,7 @@ public void testOnEventWithError() throws Exception {
A2AError jsonrpcError = (A2AError) receivedError.get();
assertEquals(-32602, jsonrpcError.getCode());
assertEquals("Invalid parameters", jsonrpcError.getMessage());
- assertEquals("\"Missing required field\"", jsonrpcError.getData());
+ assertEquals("Missing required field", jsonrpcError.getDetails().get("info"));
}
@Test
diff --git a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestErrorMapper.java b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestErrorMapper.java
index e749070af..128a81223 100644
--- a/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestErrorMapper.java
+++ b/client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestErrorMapper.java
@@ -1,5 +1,7 @@
package io.a2a.client.transport.rest;
+import java.util.HashMap;
+import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -8,8 +10,9 @@
import io.a2a.jsonrpc.common.json.JsonProcessingException;
import io.a2a.jsonrpc.common.json.JsonUtil;
import io.a2a.spec.A2AClientException;
-import io.a2a.spec.ExtendedAgentCardNotConfiguredError;
+import io.a2a.spec.A2AErrorCodes;
import io.a2a.spec.ContentTypeNotSupportedError;
+import io.a2a.spec.ExtendedAgentCardNotConfiguredError;
import io.a2a.spec.ExtensionSupportRequiredError;
import io.a2a.spec.InternalError;
import io.a2a.spec.InvalidAgentResponseError;
@@ -28,6 +31,25 @@
*/
public class RestErrorMapper {
+ private record ReasonAndMetadata(String reason, @org.jspecify.annotations.Nullable Map metadata) {}
+
+ private static final Map REASON_MAP = Map.ofEntries(
+ Map.entry("TASK_NOT_FOUND", A2AErrorCodes.TASK_NOT_FOUND),
+ Map.entry("TASK_NOT_CANCELABLE", A2AErrorCodes.TASK_NOT_CANCELABLE),
+ Map.entry("PUSH_NOTIFICATION_NOT_SUPPORTED", A2AErrorCodes.PUSH_NOTIFICATION_NOT_SUPPORTED),
+ Map.entry("UNSUPPORTED_OPERATION", A2AErrorCodes.UNSUPPORTED_OPERATION),
+ Map.entry("CONTENT_TYPE_NOT_SUPPORTED", A2AErrorCodes.CONTENT_TYPE_NOT_SUPPORTED),
+ Map.entry("INVALID_AGENT_RESPONSE", A2AErrorCodes.INVALID_AGENT_RESPONSE),
+ Map.entry("EXTENDED_AGENT_CARD_NOT_CONFIGURED", A2AErrorCodes.EXTENDED_AGENT_CARD_NOT_CONFIGURED),
+ Map.entry("EXTENSION_SUPPORT_REQUIRED", A2AErrorCodes.EXTENSION_SUPPORT_REQUIRED),
+ Map.entry("VERSION_NOT_SUPPORTED", A2AErrorCodes.VERSION_NOT_SUPPORTED),
+ Map.entry("INVALID_REQUEST", A2AErrorCodes.INVALID_REQUEST),
+ Map.entry("METHOD_NOT_FOUND", A2AErrorCodes.METHOD_NOT_FOUND),
+ Map.entry("INVALID_PARAMS", A2AErrorCodes.INVALID_PARAMS),
+ Map.entry("INTERNAL", A2AErrorCodes.INTERNAL),
+ Map.entry("JSON_PARSE", A2AErrorCodes.JSON_PARSE)
+ );
+
public static A2AClientException mapRestError(A2AHttpResponse response) {
return RestErrorMapper.mapRestError(response.body(), response.status());
}
@@ -36,13 +58,17 @@ public static A2AClientException mapRestError(String body, int code) {
try {
if (body != null && !body.isBlank()) {
JsonObject node = JsonUtil.fromJson(body, JsonObject.class);
- // Support RFC 7807 Problem Details format (type, title, details, status)
- if (node.has("type")) {
- String type = node.get("type").getAsString();
- String errorMessage = node.has("title") ? node.get("title").getAsString() : "";
- return mapRestErrorByType(type, errorMessage, code);
+ // Google Cloud API error format: { "error": { "code", "status", "message", "details" } }
+ if (node.has("error") && node.get("error").isJsonObject()) {
+ JsonObject errorObj = node.getAsJsonObject("error");
+ String errorMessage = errorObj.has("message") ? errorObj.get("message").getAsString() : "";
+ ReasonAndMetadata reasonAndMetadata = extractReasonAndMetadata(errorObj);
+ if (reasonAndMetadata != null) {
+ return mapRestErrorByReason(reasonAndMetadata.reason(), errorMessage, reasonAndMetadata.metadata());
+ }
+ return new A2AClientException(errorMessage);
}
- // Legacy format (error, message)
+ // Legacy format (error class name, message)
String className = node.has("error") ? node.get("error").getAsString() : "";
String errorMessage = node.has("message") ? node.get("message").getAsString() : "";
return mapRestErrorByClassName(className, errorMessage, code);
@@ -59,36 +85,59 @@ public static A2AClientException mapRestError(String className, String errorMess
}
/**
- * Maps RFC 7807 Problem Details error type URIs to A2A exceptions.
- *
- * Note: Error constructors receive null for code and data parameters because:
- *
- * - Error codes are defaulted by each error class (e.g., -32007 for ExtendedAgentCardNotConfiguredError)
- * - The message comes from the RFC 7807 "title" field
- * - The data field is optional and not included in basic RFC 7807 responses
- *
+ * Extracts the "reason" and "metadata" fields from the first entry in the "details" array.
+ */
+ private static @org.jspecify.annotations.Nullable ReasonAndMetadata extractReasonAndMetadata(JsonObject errorObj) {
+ if (errorObj.has("details") && errorObj.get("details").isJsonArray()) {
+ var details = errorObj.getAsJsonArray("details");
+ if (!details.isEmpty() && details.get(0).isJsonObject()) {
+ JsonObject detail = details.get(0).getAsJsonObject();
+ if (detail.has("reason")) {
+ String reason = detail.get("reason").getAsString();
+ Map metadata = null;
+ if (detail.has("metadata") && detail.get("metadata").isJsonObject()) {
+ JsonObject metaObj = detail.getAsJsonObject("metadata");
+ Map metaMap = new HashMap<>();
+ for (var entry : metaObj.entrySet()) {
+ metaMap.put(entry.getKey(), entry.getValue().getAsString());
+ }
+ metadata = metaMap;
+ }
+ return new ReasonAndMetadata(reason, metadata);
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Maps error reason strings to A2A exceptions.
*
- * @param type the RFC 7807 error type URI (e.g., "https://a2a-protocol.org/errors/task-not-found")
- * @param errorMessage the error message from the "title" field
- * @param code the HTTP status code (currently unused, kept for consistency)
+ * @param reason the error reason (e.g., "TASK_NOT_FOUND")
+ * @param errorMessage the error message
+ * @param metadata additional metadata extracted from the error details
* @return an A2AClientException wrapping the appropriate A2A error
*/
- private static A2AClientException mapRestErrorByType(String type, String errorMessage, int code) {
- return switch (type) {
- case "https://a2a-protocol.org/errors/task-not-found" -> new A2AClientException(errorMessage, new TaskNotFoundError());
- case "https://a2a-protocol.org/errors/extended-agent-card-not-configured" -> new A2AClientException(errorMessage, new ExtendedAgentCardNotConfiguredError(null, errorMessage, null));
- case "https://a2a-protocol.org/errors/content-type-not-supported" -> new A2AClientException(errorMessage, new ContentTypeNotSupportedError(null, errorMessage, null));
- case "https://a2a-protocol.org/errors/internal-error" -> new A2AClientException(errorMessage, new InternalError(errorMessage));
- case "https://a2a-protocol.org/errors/invalid-agent-response" -> new A2AClientException(errorMessage, new InvalidAgentResponseError(null, errorMessage, null));
- case "https://a2a-protocol.org/errors/invalid-params" -> new A2AClientException(errorMessage, new InvalidParamsError());
- case "https://a2a-protocol.org/errors/invalid-request" -> new A2AClientException(errorMessage, new InvalidRequestError());
- case "https://a2a-protocol.org/errors/method-not-found" -> new A2AClientException(errorMessage, new MethodNotFoundError());
- case "https://a2a-protocol.org/errors/push-notification-not-supported" -> new A2AClientException(errorMessage, new PushNotificationNotSupportedError());
- case "https://a2a-protocol.org/errors/task-not-cancelable" -> new A2AClientException(errorMessage, new TaskNotCancelableError());
- case "https://a2a-protocol.org/errors/unsupported-operation" -> new A2AClientException(errorMessage, new UnsupportedOperationError());
- case "https://a2a-protocol.org/errors/extension-support-required" -> new A2AClientException(errorMessage, new ExtensionSupportRequiredError(null, errorMessage, null));
- case "https://a2a-protocol.org/errors/version-not-supported" -> new A2AClientException(errorMessage, new VersionNotSupportedError(null, errorMessage, null));
- default -> new A2AClientException(errorMessage);
+ private static A2AClientException mapRestErrorByReason(String reason, String errorMessage, @org.jspecify.annotations.Nullable Map metadata) {
+ A2AErrorCodes errorCode = REASON_MAP.get(reason);
+ if (errorCode == null) {
+ return new A2AClientException(errorMessage);
+ }
+ return switch (errorCode) {
+ case TASK_NOT_FOUND -> new A2AClientException(errorMessage, new TaskNotFoundError(errorMessage, metadata));
+ case TASK_NOT_CANCELABLE -> new A2AClientException(errorMessage, new TaskNotCancelableError(null, errorMessage, metadata));
+ case PUSH_NOTIFICATION_NOT_SUPPORTED -> new A2AClientException(errorMessage, new PushNotificationNotSupportedError(null, errorMessage, metadata));
+ case UNSUPPORTED_OPERATION -> new A2AClientException(errorMessage, new UnsupportedOperationError(null, errorMessage, metadata));
+ case CONTENT_TYPE_NOT_SUPPORTED -> new A2AClientException(errorMessage, new ContentTypeNotSupportedError(null, errorMessage, metadata));
+ case INVALID_AGENT_RESPONSE -> new A2AClientException(errorMessage, new InvalidAgentResponseError(null, errorMessage, metadata));
+ case EXTENDED_AGENT_CARD_NOT_CONFIGURED -> new A2AClientException(errorMessage, new ExtendedAgentCardNotConfiguredError(null, errorMessage, metadata));
+ case EXTENSION_SUPPORT_REQUIRED -> new A2AClientException(errorMessage, new ExtensionSupportRequiredError(null, errorMessage, metadata));
+ case VERSION_NOT_SUPPORTED -> new A2AClientException(errorMessage, new VersionNotSupportedError(null, errorMessage, metadata));
+ case INVALID_REQUEST -> new A2AClientException(errorMessage, new InvalidRequestError(null, errorMessage, metadata));
+ case JSON_PARSE -> new A2AClientException(errorMessage, new JSONParseError(null, errorMessage, metadata));
+ case METHOD_NOT_FOUND -> new A2AClientException(errorMessage, new MethodNotFoundError(null, errorMessage, metadata));
+ case INVALID_PARAMS -> new A2AClientException(errorMessage, new InvalidParamsError(null, errorMessage, metadata));
+ case INTERNAL -> new A2AClientException(errorMessage, new InternalError(null, errorMessage, metadata));
};
}
@@ -96,9 +145,9 @@ private static A2AClientException mapRestErrorByClassName(String className, Stri
return switch (className) {
case "io.a2a.spec.TaskNotFoundError" -> new A2AClientException(errorMessage, new TaskNotFoundError());
case "io.a2a.spec.ExtendedCardNotConfiguredError" -> new A2AClientException(errorMessage, new ExtendedAgentCardNotConfiguredError(null, errorMessage, null));
- case "io.a2a.spec.ContentTypeNotSupportedError" -> new A2AClientException(errorMessage, new ContentTypeNotSupportedError(null, null, errorMessage));
+ case "io.a2a.spec.ContentTypeNotSupportedError" -> new A2AClientException(errorMessage, new ContentTypeNotSupportedError(null, errorMessage, null));
case "io.a2a.spec.InternalError" -> new A2AClientException(errorMessage, new InternalError(errorMessage));
- case "io.a2a.spec.InvalidAgentResponseError" -> new A2AClientException(errorMessage, new InvalidAgentResponseError(null, null, errorMessage));
+ case "io.a2a.spec.InvalidAgentResponseError" -> new A2AClientException(errorMessage, new InvalidAgentResponseError(null, errorMessage, null));
case "io.a2a.spec.InvalidParamsError" -> new A2AClientException(errorMessage, new InvalidParamsError());
case "io.a2a.spec.InvalidRequestError" -> new A2AClientException(errorMessage, new InvalidRequestError());
case "io.a2a.spec.JSONParseError" -> new A2AClientException(errorMessage, new JSONParseError());
diff --git a/jsonrpc-common/src/main/java/io/a2a/jsonrpc/common/json/JsonUtil.java b/jsonrpc-common/src/main/java/io/a2a/jsonrpc/common/json/JsonUtil.java
index 9e8fb057f..3967cbf80 100644
--- a/jsonrpc-common/src/main/java/io/a2a/jsonrpc/common/json/JsonUtil.java
+++ b/jsonrpc-common/src/main/java/io/a2a/jsonrpc/common/json/JsonUtil.java
@@ -1,23 +1,12 @@
package io.a2a.jsonrpc.common.json;
-import static io.a2a.jsonrpc.common.json.JsonUtil.A2AErrorTypeAdapter.THROWABLE_MARKER_FIELD;
-import static io.a2a.spec.A2AErrorCodes.CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.INTERNAL_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.INVALID_AGENT_RESPONSE_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.INVALID_PARAMS_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.INVALID_REQUEST_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.JSON_PARSE_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.METHOD_NOT_FOUND_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.PUSH_NOTIFICATION_NOT_SUPPORTED_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.TASK_NOT_CANCELABLE_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.TASK_NOT_FOUND_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.UNSUPPORTED_OPERATION_ERROR_CODE;
+import static io.a2a.jsonrpc.common.json.JsonUtil.ThrowableTypeAdapter.THROWABLE_MARKER_FIELD;
+import io.a2a.spec.A2AErrorCodes;
import static io.a2a.spec.DataPart.DATA;
import static io.a2a.spec.TextPart.TEXT;
import static java.lang.String.format;
import static java.util.Collections.emptyMap;
-import java.io.StringReader;
import java.lang.reflect.Type;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
@@ -44,6 +33,8 @@
import io.a2a.spec.APIKeySecurityScheme;
import io.a2a.spec.ContentTypeNotSupportedError;
import io.a2a.spec.DataPart;
+import io.a2a.spec.ExtendedAgentCardNotConfiguredError;
+import io.a2a.spec.ExtensionSupportRequiredError;
import io.a2a.spec.FileContent;
import io.a2a.spec.FilePart;
import io.a2a.spec.FileWithBytes;
@@ -70,6 +61,7 @@
import io.a2a.spec.TaskStatusUpdateEvent;
import io.a2a.spec.TextPart;
import io.a2a.spec.UnsupportedOperationError;
+import io.a2a.spec.VersionNotSupportedError;
import org.jspecify.annotations.Nullable;
@@ -218,6 +210,8 @@ OffsetDateTime read(JsonReader in) throws java.io.IOException {
*/
static class ThrowableTypeAdapter extends TypeAdapter {
+ static final String THROWABLE_MARKER_FIELD = "__throwable";
+
@Override
public void write(JsonWriter out, Throwable value) throws java.io.IOException {
if (value == null) {
@@ -305,10 +299,8 @@ Throwable read(JsonReader in) throws java.io.IOException {
*/
static class A2AErrorTypeAdapter extends TypeAdapter {
- private static final ThrowableTypeAdapter THROWABLE_ADAPTER = new ThrowableTypeAdapter();
- static final String THROWABLE_MARKER_FIELD = "__throwable";
private static final String CODE_FIELD = "code";
- private static final String DATA_FIELD = "data";
+ private static final String DETAILS_FIELD = "details";
private static final String MESSAGE_FIELD = "message";
private static final String TYPE_FIELD = "type";
@@ -321,15 +313,9 @@ public void write(JsonWriter out, A2AError value) throws java.io.IOException {
out.beginObject();
out.name(CODE_FIELD).value(value.getCode());
out.name(MESSAGE_FIELD).value(value.getMessage());
- if (value.getData() != null) {
- out.name(DATA_FIELD);
- // If data is a Throwable, use ThrowableTypeAdapter to avoid reflection issues
- if (value.getData() instanceof Throwable throwable) {
- THROWABLE_ADAPTER.write(out, throwable);
- } else {
- // Use Gson to serialize the data field for non-Throwable types
- OBJECT_MAPPER.toJson(value.getData(), Object.class, out);
- }
+ if (!value.getDetails().isEmpty()) {
+ out.name(DETAILS_FIELD);
+ OBJECT_MAPPER.toJson(value.getDetails(), Map.class, out);
}
out.endObject();
}
@@ -344,7 +330,7 @@ A2AError read(JsonReader in) throws java.io.IOException {
Integer code = null;
String message = null;
- Object data = null;
+ Map details = null;
in.beginObject();
while (in.hasNext()) {
@@ -354,9 +340,9 @@ A2AError read(JsonReader in) throws java.io.IOException {
code = in.nextInt();
case MESSAGE_FIELD ->
message = in.nextString();
- case DATA_FIELD -> {
- // Read data as a generic object (could be string, number, object, etc.)
- data = readDataValue(in);
+ case DETAILS_FIELD -> {
+ // Read details as a map
+ details = readDetailsValue(in);
}
default ->
in.skipValue();
@@ -365,82 +351,53 @@ A2AError read(JsonReader in) throws java.io.IOException {
in.endObject();
// Create the appropriate subclass based on the error code
- return createErrorInstance(code, message, data);
+ return createErrorInstance(code, message, details);
}
/**
- * Reads the data field value, which can be of any JSON type.
+ * Reads the details field value as a map.
*/
+ @SuppressWarnings("unchecked")
private @Nullable
- Object readDataValue(JsonReader in) throws java.io.IOException {
- return switch (in.peek()) {
- case STRING ->
- in.nextString();
- case NUMBER ->
- in.nextDouble();
- case BOOLEAN ->
- in.nextBoolean();
- case NULL -> {
- in.nextNull();
- yield null;
- }
- case BEGIN_OBJECT -> {
- // Parse as JsonElement to check if it's a Throwable
- com.google.gson.JsonElement element = com.google.gson.JsonParser.parseReader(in);
- if (element.isJsonObject()) {
- com.google.gson.JsonObject obj = element.getAsJsonObject();
- // Check if it has the structure of a serialized Throwable (type + message)
- if (obj.has(TYPE_FIELD) && obj.has(MESSAGE_FIELD) && obj.has(THROWABLE_MARKER_FIELD)) {
- // Deserialize as Throwable using ThrowableTypeAdapter
- yield THROWABLE_ADAPTER.read(new JsonReader(new StringReader(element.toString())));
- }
- }
- // Otherwise, deserialize as generic object
- yield OBJECT_MAPPER.fromJson(element, Object.class);
- }
- case BEGIN_ARRAY ->
- // For arrays, read as raw JSON using Gson
- OBJECT_MAPPER.fromJson(in, Object.class);
- default -> {
- in.skipValue();
- yield null;
- }
- };
+ Map readDetailsValue(JsonReader in) throws java.io.IOException {
+ if (in.peek() == com.google.gson.stream.JsonToken.NULL) {
+ in.nextNull();
+ return null;
+ }
+ if (in.peek() == com.google.gson.stream.JsonToken.BEGIN_OBJECT) {
+ return (Map) OBJECT_MAPPER.fromJson(in, Map.class);
+ }
+ in.skipValue();
+ return null;
}
/**
* Creates the appropriate A2AError subclass based on the error code.
*/
- private A2AError createErrorInstance(@Nullable Integer code, @Nullable String message, @Nullable Object data) {
+ private A2AError createErrorInstance(@Nullable Integer code, @Nullable String message, @Nullable Map details) {
if (code == null) {
throw new JsonSyntaxException("A2AError must have a code field");
}
- return switch (code) {
- case JSON_PARSE_ERROR_CODE ->
- new JSONParseError(code, message, data);
- case INVALID_REQUEST_ERROR_CODE ->
- new InvalidRequestError(code, message, data);
- case METHOD_NOT_FOUND_ERROR_CODE ->
- new MethodNotFoundError(code, message, data);
- case INVALID_PARAMS_ERROR_CODE ->
- new InvalidParamsError(code, message, data);
- case INTERNAL_ERROR_CODE ->
- new io.a2a.spec.InternalError(code, message, data);
- case TASK_NOT_FOUND_ERROR_CODE ->
- new TaskNotFoundError(message, data);
- case TASK_NOT_CANCELABLE_ERROR_CODE ->
- new TaskNotCancelableError(code, message, data);
- case PUSH_NOTIFICATION_NOT_SUPPORTED_ERROR_CODE ->
- new PushNotificationNotSupportedError(code, message, data);
- case UNSUPPORTED_OPERATION_ERROR_CODE ->
- new UnsupportedOperationError(code, message, data);
- case CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE ->
- new ContentTypeNotSupportedError(code, message, data);
- case INVALID_AGENT_RESPONSE_ERROR_CODE ->
- new InvalidAgentResponseError(code, message, data);
- default ->
- new A2AError(code, message == null ? "" : message, data);
+ A2AErrorCodes errorCode = A2AErrorCodes.fromCode(code);
+ if (errorCode == null) {
+ return new A2AError(code, message == null ? "" : message, details);
+ }
+ return switch (errorCode) {
+ case JSON_PARSE -> new JSONParseError(code, message, details);
+ case INVALID_REQUEST -> new InvalidRequestError(code, message, details);
+ case METHOD_NOT_FOUND -> new MethodNotFoundError(code, message, details);
+ case INVALID_PARAMS -> new InvalidParamsError(code, message, details);
+ case INTERNAL -> new io.a2a.spec.InternalError(code, message, details);
+ case TASK_NOT_FOUND -> new TaskNotFoundError(message, details);
+ case TASK_NOT_CANCELABLE -> new TaskNotCancelableError(code, message, details);
+ case PUSH_NOTIFICATION_NOT_SUPPORTED -> new PushNotificationNotSupportedError(code, message, details);
+ case UNSUPPORTED_OPERATION -> new UnsupportedOperationError(code, message, details);
+ case CONTENT_TYPE_NOT_SUPPORTED -> new ContentTypeNotSupportedError(code, message, details);
+ case INVALID_AGENT_RESPONSE -> new InvalidAgentResponseError(code, message, details);
+ case EXTENDED_AGENT_CARD_NOT_CONFIGURED -> new ExtendedAgentCardNotConfiguredError(code, message, details);
+ case EXTENSION_SUPPORT_REQUIRED -> new ExtensionSupportRequiredError(code, message, details);
+ case VERSION_NOT_SUPPORTED -> new VersionNotSupportedError(code, message, details);
};
}
}
diff --git a/jsonrpc-common/src/test/java/io/a2a/jsonrpc/common/json/A2AErrorSerializationTest.java b/jsonrpc-common/src/test/java/io/a2a/jsonrpc/common/json/A2AErrorSerializationTest.java
index ff7ee5cc1..734ff47e0 100644
--- a/jsonrpc-common/src/test/java/io/a2a/jsonrpc/common/json/A2AErrorSerializationTest.java
+++ b/jsonrpc-common/src/test/java/io/a2a/jsonrpc/common/json/A2AErrorSerializationTest.java
@@ -1,22 +1,12 @@
package io.a2a.jsonrpc.common.json;
-import static io.a2a.spec.A2AErrorCodes.CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.INTERNAL_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.INVALID_AGENT_RESPONSE_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.INVALID_PARAMS_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.INVALID_REQUEST_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.JSON_PARSE_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.METHOD_NOT_FOUND_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.PUSH_NOTIFICATION_NOT_SUPPORTED_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.TASK_NOT_CANCELABLE_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.TASK_NOT_FOUND_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.UNSUPPORTED_OPERATION_ERROR_CODE;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import java.util.List;
import io.a2a.spec.A2AError;
+import io.a2a.spec.A2AErrorCodes;
import io.a2a.spec.ContentTypeNotSupportedError;
import io.a2a.spec.InternalError;
import io.a2a.spec.InvalidAgentResponseError;
@@ -35,22 +25,23 @@ public class A2AErrorSerializationTest {
@Test
public void shouldDeserializeToCorrectA2AErrorSubclass() throws JsonProcessingException {
String jsonTemplate = """
- {"code": %s, "message": "error", "data": "anything"}
+ {"code": %s, "message": "error", "details": {"key": "anything"}}
""";
record ErrorCase(int code, Class extends A2AError> clazz) {}
- List cases = List.of(new ErrorCase(JSON_PARSE_ERROR_CODE, JSONParseError.class),
- new ErrorCase(INVALID_REQUEST_ERROR_CODE, InvalidRequestError.class),
- new ErrorCase(METHOD_NOT_FOUND_ERROR_CODE, MethodNotFoundError.class),
- new ErrorCase(INVALID_PARAMS_ERROR_CODE, InvalidParamsError.class),
- new ErrorCase(INTERNAL_ERROR_CODE, InternalError.class),
- new ErrorCase(PUSH_NOTIFICATION_NOT_SUPPORTED_ERROR_CODE, PushNotificationNotSupportedError.class),
- new ErrorCase(UNSUPPORTED_OPERATION_ERROR_CODE, UnsupportedOperationError.class),
- new ErrorCase(CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE, ContentTypeNotSupportedError.class),
- new ErrorCase(INVALID_AGENT_RESPONSE_ERROR_CODE, InvalidAgentResponseError.class),
- new ErrorCase(TASK_NOT_CANCELABLE_ERROR_CODE, TaskNotCancelableError.class),
- new ErrorCase(TASK_NOT_FOUND_ERROR_CODE, TaskNotFoundError.class),
+ List cases = List.of(
+ new ErrorCase(A2AErrorCodes.JSON_PARSE.code(), JSONParseError.class),
+ new ErrorCase(A2AErrorCodes.INVALID_REQUEST.code(), InvalidRequestError.class),
+ new ErrorCase(A2AErrorCodes.METHOD_NOT_FOUND.code(), MethodNotFoundError.class),
+ new ErrorCase(A2AErrorCodes.INVALID_PARAMS.code(), InvalidParamsError.class),
+ new ErrorCase(A2AErrorCodes.INTERNAL.code(), InternalError.class),
+ new ErrorCase(A2AErrorCodes.PUSH_NOTIFICATION_NOT_SUPPORTED.code(), PushNotificationNotSupportedError.class),
+ new ErrorCase(A2AErrorCodes.UNSUPPORTED_OPERATION.code(), UnsupportedOperationError.class),
+ new ErrorCase(A2AErrorCodes.CONTENT_TYPE_NOT_SUPPORTED.code(), ContentTypeNotSupportedError.class),
+ new ErrorCase(A2AErrorCodes.INVALID_AGENT_RESPONSE.code(), InvalidAgentResponseError.class),
+ new ErrorCase(A2AErrorCodes.TASK_NOT_CANCELABLE.code(), TaskNotCancelableError.class),
+ new ErrorCase(A2AErrorCodes.TASK_NOT_FOUND.code(), TaskNotFoundError.class),
new ErrorCase(Integer.MAX_VALUE, A2AError.class) // Any unknown code will be treated as A2AError
);
@@ -59,7 +50,7 @@ record ErrorCase(int code, Class extends A2AError> clazz) {}
A2AError error = JsonUtil.fromJson(json, A2AError.class);
assertInstanceOf(errorCase.clazz(), error);
assertEquals("error", error.getMessage());
- assertEquals("anything", error.getData().toString());
+ assertEquals("anything", error.getDetails().get("key"));
}
}
diff --git a/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java b/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java
index 6914dc5f9..eaf6c02b3 100644
--- a/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java
+++ b/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java
@@ -580,7 +580,7 @@ private String extractTenant(RoutingContext rc) {
* "error": {
* "code": -32602,
* "message": "Invalid params",
- * "data": { ... }
+ * "details": { ... }
* }
* }
* }
diff --git a/reference/rest/src/test/java/io/a2a/server/rest/quarkus/A2AServerRoutesTest.java b/reference/rest/src/test/java/io/a2a/server/rest/quarkus/A2AServerRoutesTest.java
index 80a30e2f6..09a45c828 100644
--- a/reference/rest/src/test/java/io/a2a/server/rest/quarkus/A2AServerRoutesTest.java
+++ b/reference/rest/src/test/java/io/a2a/server/rest/quarkus/A2AServerRoutesTest.java
@@ -447,7 +447,7 @@ public void testSendMessage_UnsupportedContentType_ReturnsContentTypeNotSupporte
HTTPRestResponse mockErrorResponse = mock(HTTPRestResponse.class);
when(mockErrorResponse.getStatusCode()).thenReturn(415);
when(mockErrorResponse.getContentType()).thenReturn("application/problem+json");
- when(mockErrorResponse.getBody()).thenReturn("{\"type\":\"https://a2a-protocol.org/errors/content-type-not-supported\"}");
+ 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(mockRestHandler.createErrorResponse(any(ContentTypeNotSupportedError.class))).thenReturn(mockErrorResponse);
when(mockRequest.getHeader(any(CharSequence.class))).thenReturn("text/plain");
@@ -465,7 +465,7 @@ public void testSendMessageStreaming_UnsupportedContentType_ReturnsContentTypeNo
HTTPRestResponse mockErrorResponse = mock(HTTPRestResponse.class);
when(mockErrorResponse.getStatusCode()).thenReturn(415);
when(mockErrorResponse.getContentType()).thenReturn("application/problem+json");
- when(mockErrorResponse.getBody()).thenReturn("{\"type\":\"https://a2a-protocol.org/errors/content-type-not-supported\"}");
+ 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(mockRestHandler.createErrorResponse(any(ContentTypeNotSupportedError.class))).thenReturn(mockErrorResponse);
when(mockRequest.getHeader(any(CharSequence.class))).thenReturn("text/plain");
@@ -483,7 +483,7 @@ public void testSendMessage_UnsupportedProtocolVersion_ReturnsVersionNotSupporte
HTTPRestResponse mockErrorResponse = mock(HTTPRestResponse.class);
when(mockErrorResponse.getStatusCode()).thenReturn(400);
when(mockErrorResponse.getContentType()).thenReturn("application/problem+json");
- when(mockErrorResponse.getBody()).thenReturn("{\"type\":\"https://a2a-protocol.org/errors/version-not-supported\"}");
+ when(mockErrorResponse.getBody()).thenReturn("{\"error\":{\"code\":400,\"status\":\"UNIMPLEMENTED\",\"message\":\"Protocol version not supported\",\"details\":[{\"reason\":\"VERSION_NOT_SUPPORTED\",\"domain\":\"a2a-protocol.org\"}]}}");
when(mockRequest.getHeader(any(CharSequence.class))).thenReturn("application/json");
when(mockRestHandler.sendMessage(any(ServerCallContext.class), anyString(), anyString()))
.thenReturn(mockErrorResponse);
diff --git a/reference/rest/src/test/java/io/a2a/server/rest/quarkus/QuarkusA2ARestTest.java b/reference/rest/src/test/java/io/a2a/server/rest/quarkus/QuarkusA2ARestTest.java
index 8e234f9f9..15f5178a3 100644
--- a/reference/rest/src/test/java/io/a2a/server/rest/quarkus/QuarkusA2ARestTest.java
+++ b/reference/rest/src/test/java/io/a2a/server/rest/quarkus/QuarkusA2ARestTest.java
@@ -42,8 +42,8 @@ public void testSendMessageWithUnsupportedContentType() throws Exception {
.build();
HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
Assertions.assertEquals(415, response.statusCode());
- Assertions.assertTrue(response.body().contains("content-type-not-supported"),
- "Expected content-type-not-supported in response body: " + response.body());
+ Assertions.assertTrue(response.body().contains("CONTENT_TYPE_NOT_SUPPORTED"),
+ "Expected CONTENT_TYPE_NOT_SUPPORTED in response body: " + response.body());
}
@Test
@@ -59,8 +59,8 @@ public void testSendMessageWithUnsupportedProtocolVersion() throws Exception {
.build();
HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
Assertions.assertEquals(400, response.statusCode());
- Assertions.assertTrue(response.body().contains("version-not-supported"),
- "Expected version-not-supported in response body: " + response.body());
+ Assertions.assertTrue(response.body().contains("VERSION_NOT_SUPPORTED"),
+ "Expected VERSION_NOT_SUPPORTED in response body: " + response.body());
}
@Test
diff --git a/spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java b/spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java
index 9ea4a2a38..a21008bda 100644
--- a/spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java
+++ b/spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java
@@ -1,25 +1,13 @@
package io.a2a.grpc.utils;
-import static io.a2a.spec.A2AErrorCodes.CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.EXTENDED_AGENT_CARD_NOT_CONFIGURED_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.EXTENSION_SUPPORT_REQUIRED_ERROR;
-import static io.a2a.spec.A2AErrorCodes.INTERNAL_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.INVALID_AGENT_RESPONSE_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.INVALID_PARAMS_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.INVALID_REQUEST_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.JSON_PARSE_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.METHOD_NOT_FOUND_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.PUSH_NOTIFICATION_NOT_SUPPORTED_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.TASK_NOT_CANCELABLE_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.TASK_NOT_FOUND_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.UNSUPPORTED_OPERATION_ERROR_CODE;
-import static io.a2a.spec.A2AErrorCodes.VERSION_NOT_SUPPORTED_ERROR_CODE;
+import io.a2a.spec.A2AErrorCodes;
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 java.io.IOException;
import java.io.StringWriter;
+import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -399,45 +387,37 @@ public static A2AResponse> parseError(JsonObject error, Object id, String meth
}
}
+ @SuppressWarnings("unchecked")
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;
- String data = error.has("data") ? error.get("data").toString() : null;
+ Map details = null;
+ if (error.has("details") && error.get("details").isJsonObject()) {
+ details =GSON.fromJson(error.get("details"), Map.class);
+ }
if (code != null) {
- switch (code) {
- case JSON_PARSE_ERROR_CODE:
- return new JSONParseError(code, message, data);
- case INVALID_REQUEST_ERROR_CODE:
- return new InvalidRequestError(code, message, data);
- case METHOD_NOT_FOUND_ERROR_CODE:
- return new MethodNotFoundError(code, message, data);
- case INVALID_PARAMS_ERROR_CODE:
- return new InvalidParamsError(code, message, data);
- case INTERNAL_ERROR_CODE:
- return new io.a2a.spec.InternalError(code, message, data);
- case PUSH_NOTIFICATION_NOT_SUPPORTED_ERROR_CODE:
- return new PushNotificationNotSupportedError(code, message, data);
- case UNSUPPORTED_OPERATION_ERROR_CODE:
- return new UnsupportedOperationError(code, message, data);
- case CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE:
- return new ContentTypeNotSupportedError(code, message, data);
- case INVALID_AGENT_RESPONSE_ERROR_CODE:
- return new InvalidAgentResponseError(code, message, data);
- case EXTENDED_AGENT_CARD_NOT_CONFIGURED_ERROR_CODE:
- return new ExtendedAgentCardNotConfiguredError(code, message, data);
- case EXTENSION_SUPPORT_REQUIRED_ERROR:
- return new ExtensionSupportRequiredError(code, message, data);
- case VERSION_NOT_SUPPORTED_ERROR_CODE:
- return new VersionNotSupportedError(code, message, data);
- case TASK_NOT_CANCELABLE_ERROR_CODE:
- return new TaskNotCancelableError(code, message, data);
- case TASK_NOT_FOUND_ERROR_CODE:
- return new TaskNotFoundError(message, data);
- default:
- return new A2AError(code, message == null ? "": message, data);
- }
+ A2AErrorCodes errorCode = A2AErrorCodes.fromCode(code);
+ if (errorCode != null) {
+ return switch (errorCode) {
+ case JSON_PARSE -> new JSONParseError(code, message, details);
+ case INVALID_REQUEST -> new InvalidRequestError(code, message, details);
+ case METHOD_NOT_FOUND -> new MethodNotFoundError(code, message, details);
+ case INVALID_PARAMS -> new InvalidParamsError(code, message, details);
+ case INTERNAL -> new io.a2a.spec.InternalError(code, message, details);
+ case PUSH_NOTIFICATION_NOT_SUPPORTED -> new PushNotificationNotSupportedError(code, message, details);
+ case UNSUPPORTED_OPERATION -> new UnsupportedOperationError(code, message, details);
+ case CONTENT_TYPE_NOT_SUPPORTED -> new ContentTypeNotSupportedError(code, message, details);
+ case INVALID_AGENT_RESPONSE -> new InvalidAgentResponseError(code, message, details);
+ case EXTENDED_AGENT_CARD_NOT_CONFIGURED -> new ExtendedAgentCardNotConfiguredError(code, message, details);
+ case EXTENSION_SUPPORT_REQUIRED -> new ExtensionSupportRequiredError(code, message, details);
+ case VERSION_NOT_SUPPORTED -> new VersionNotSupportedError(code, message, details);
+ case TASK_NOT_CANCELABLE -> new TaskNotCancelableError(code, message, details);
+ case TASK_NOT_FOUND -> new TaskNotFoundError(message, details);
+ };
+ }
+ return new A2AError(code, message == null ? "" : message, details);
}
- return new A2AError(INTERNAL_ERROR_CODE, message == null ? "": message, data);
+ return new A2AError(A2AErrorCodes.INTERNAL.code(), message == null ? "" : message, details);
}
protected static void parseRequestBody(JsonElement jsonRpc, com.google.protobuf.Message.Builder builder, Object id) throws JsonProcessingException {
@@ -625,8 +605,9 @@ public static String toJsonRPCErrorResponse(Object requestId, A2AError error) {
output.beginObject();
output.name("code").value(error.getCode());
output.name("message").value(error.getMessage());
- if (error.getData() != null) {
- output.name("data").value(error.getData().toString());
+ if (!error.getDetails().isEmpty()) {
+ output.name("details");
+ GSON.toJson(error.getDetails(), Map.class, output);
}
output.endObject();
output.endObject();
diff --git a/spec/src/main/java/io/a2a/spec/A2AError.java b/spec/src/main/java/io/a2a/spec/A2AError.java
index c81bbbe32..eaec2b0fe 100644
--- a/spec/src/main/java/io/a2a/spec/A2AError.java
+++ b/spec/src/main/java/io/a2a/spec/A2AError.java
@@ -1,5 +1,7 @@
package io.a2a.spec;
+import java.util.Map;
+
import io.a2a.util.Assert;
import org.jspecify.annotations.Nullable;
@@ -35,24 +37,24 @@ public class A2AError extends RuntimeException implements Event {
private final Integer code;
/**
- * Additional error information (structure defined by the error code).
+ * Additional error details as key-value pairs.
*/
- private final @Nullable Object data;
+ private final Map details;
/**
- * Constructs a JSON-RPC error with the specified code, message, and optional data.
+ * Constructs a JSON-RPC error with the specified code, message, and optional details.
*
* This constructor is used by Jackson for JSON deserialization.
*
* @param code the numeric error code (required, see JSON-RPC 2.0 spec for standard codes)
* @param message the human-readable error message (required)
- * @param data additional error information, structure defined by the error code (optional)
+ * @param details additional error details as key-value pairs (defaults to empty map if null)
* @throws IllegalArgumentException if code or message is null
*/
- public A2AError(Integer code, String message, @Nullable Object data) {
+ public A2AError(Integer code, String message, @Nullable Map details) {
super(Assert.checkNotNullParam("message", message));
this.code = Assert.checkNotNullParam("code", code);
- this.data = data;
+ this.details = details == null ? Map.of() : Map.copyOf(details);
}
/**
@@ -75,15 +77,11 @@ public Integer getCode() {
}
/**
- * Gets additional information about the error.
- *
- * The structure and type of the data field is defined by the specific error code.
- * It may contain detailed debugging information, validation errors, or other
- * context-specific data to help diagnose the error.
+ * Gets additional details about the error as key-value pairs.
*
- * @return the error data, or null if not provided
+ * @return the error details, never null (empty map if no details provided)
*/
- public @Nullable Object getData() {
- return data;
+ public Map getDetails() {
+ return details;
}
}
diff --git a/spec/src/main/java/io/a2a/spec/A2AErrorCodes.java b/spec/src/main/java/io/a2a/spec/A2AErrorCodes.java
index 8eb4c6584..1143d7dfc 100644
--- a/spec/src/main/java/io/a2a/spec/A2AErrorCodes.java
+++ b/spec/src/main/java/io/a2a/spec/A2AErrorCodes.java
@@ -1,51 +1,115 @@
package io.a2a.spec;
+import org.jspecify.annotations.Nullable;
+
/**
* All the error codes for A2A errors.
+ *
+ * Each constant provides:
+ *
+ * - {@link #code()} - the JSON-RPC error code
+ * - {@link #grpcStatus()} - the corresponding gRPC status name
+ * - {@link #httpCode()} - the HTTP status code
+ *
+ *
+ * @see A2A Protocol Specification - Error Code Mappings
*/
-public interface A2AErrorCodes {
+public enum A2AErrorCodes {
/** Error code indicating the requested task was not found (-32001). */
- int TASK_NOT_FOUND_ERROR_CODE = -32001;
+ TASK_NOT_FOUND(-32001, "NOT_FOUND", 404),
/** Error code indicating the task cannot be canceled in its current state (-32002). */
- int TASK_NOT_CANCELABLE_ERROR_CODE = -32002;
+ TASK_NOT_CANCELABLE(-32002, "FAILED_PRECONDITION", 409),
/** Error code indicating push notifications are not supported by this agent (-32003). */
- int PUSH_NOTIFICATION_NOT_SUPPORTED_ERROR_CODE = -32003;
+ PUSH_NOTIFICATION_NOT_SUPPORTED(-32003, "UNIMPLEMENTED", 400),
/** Error code indicating the requested operation is not supported (-32004). */
- int UNSUPPORTED_OPERATION_ERROR_CODE = -32004;
+ UNSUPPORTED_OPERATION(-32004, "UNIMPLEMENTED", 400),
/** Error code indicating the content type is not supported (-32005). */
- int CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE = -32005;
+ CONTENT_TYPE_NOT_SUPPORTED(-32005, "INVALID_ARGUMENT", 415),
/** Error code indicating the agent returned an invalid response (-32006). */
- int INVALID_AGENT_RESPONSE_ERROR_CODE = -32006;
+ INVALID_AGENT_RESPONSE(-32006, "INTERNAL", 502),
/** Error code indicating extended agent card is not configured (-32007). */
- int EXTENDED_AGENT_CARD_NOT_CONFIGURED_ERROR_CODE = -32007;
+ EXTENDED_AGENT_CARD_NOT_CONFIGURED(-32007, "FAILED_PRECONDITION", 400),
/** Error code indicating client requested use of an extension marked as required: true in the Agent Card
* but the client did not declare support for it in the request (-32008). */
- int EXTENSION_SUPPORT_REQUIRED_ERROR = -32008;
+ EXTENSION_SUPPORT_REQUIRED(-32008, "FAILED_PRECONDITION", 400),
/** Error code indicating the A2A protocol version specified in the request (via A2A-Version service parameter)
* is not supported by the agent (-32009). */
- int VERSION_NOT_SUPPORTED_ERROR_CODE = -32009;
+ VERSION_NOT_SUPPORTED(-32009, "UNIMPLEMENTED", 400),
/** JSON-RPC error code for invalid request structure (-32600). */
- int INVALID_REQUEST_ERROR_CODE = -32600;
+ INVALID_REQUEST(-32600, "INVALID_ARGUMENT", 400),
/** JSON-RPC error code for method not found (-32601). */
- int METHOD_NOT_FOUND_ERROR_CODE = -32601;
+ METHOD_NOT_FOUND(-32601, "NOT_FOUND", 404),
/** JSON-RPC error code for invalid method parameters (-32602). */
- int INVALID_PARAMS_ERROR_CODE = -32602;
+ INVALID_PARAMS(-32602, "INVALID_ARGUMENT", 422),
/** JSON-RPC error code for internal server errors (-32603). */
- int INTERNAL_ERROR_CODE = -32603;
+ INTERNAL(-32603, "INTERNAL", 500),
/** JSON-RPC error code for JSON parsing errors (-32700). */
- int JSON_PARSE_ERROR_CODE = -32700;
+ JSON_PARSE(-32700, "INVALID_ARGUMENT", 400);
+
+ private final int code;
+ private final String grpcStatus;
+ private final int httpCode;
+
+ A2AErrorCodes(int code, String grpcStatus, int httpCode) {
+ this.code = code;
+ this.grpcStatus = grpcStatus;
+ this.httpCode = httpCode;
+ }
+
+ /**
+ * Returns the JSON-RPC error code.
+ *
+ * @return the numeric error code
+ */
+ public int code() {
+ return code;
+ }
+
+ /**
+ * Returns the corresponding gRPC status name.
+ *
+ * @return the gRPC status string (e.g., "NOT_FOUND", "INTERNAL")
+ */
+ public String grpcStatus() {
+ return grpcStatus;
+ }
+
+ /**
+ * Returns the HTTP status code.
+ *
+ * @return the HTTP status code
+ */
+ public int httpCode() {
+ return httpCode;
+ }
+
+ /**
+ * Looks up an error code enum constant by its JSON-RPC numeric code.
+ *
+ * @param code the JSON-RPC error code
+ * @return the matching enum constant, or {@code null} if not found
+ */
+ public static @Nullable A2AErrorCodes fromCode(int code) {
+ for (A2AErrorCodes e : values()) {
+ if (e.code == code) {
+ return e;
+ }
+ }
+ return null;
+ }
+
}
diff --git a/spec/src/main/java/io/a2a/spec/A2AProtocolError.java b/spec/src/main/java/io/a2a/spec/A2AProtocolError.java
index b03ba4080..dfa4e961e 100644
--- a/spec/src/main/java/io/a2a/spec/A2AProtocolError.java
+++ b/spec/src/main/java/io/a2a/spec/A2AProtocolError.java
@@ -1,46 +1,28 @@
package io.a2a.spec;
+import java.util.Map;
+
import org.jspecify.annotations.Nullable;
/**
- * Represents a protocol-level error in the A2A Protocol with a reference URL.
+ * Represents a protocol-level error in the A2A Protocol.
*
- * This error extends {@link A2AError} to include a URL field that provides a reference
- * to documentation, specification details, or additional context about the protocol error.
- * This is particularly useful for protocol version mismatches, unsupported features, or
- * other situations where pointing to the protocol specification would help diagnose the issue.
+ * This error extends {@link A2AError} to distinguish A2A protocol-specific errors
+ * from standard JSON-RPC errors. Protocol errors have dedicated error codes in the
+ * A2A specification.
*
* @see A2AError for the base error implementation
*/
public class A2AProtocolError extends A2AError {
/**
- * URL reference for additional information about this protocol error.
- */
- private final @Nullable String url;
-
- /**
- * Constructs a protocol error with the specified code, message, data, and reference URL.
+ * Constructs a protocol error with the specified code, message, and details.
*
* @param code the numeric error code (required, see JSON-RPC 2.0 spec for standard codes)
* @param message the human-readable error message (required)
- * @param data additional error information, structure defined by the error code (optional)
- * @param url URL reference providing additional context about this protocol error (optional)
- */
- public A2AProtocolError(Integer code, String message, @Nullable Object data, @Nullable String url) {
- super(code, message, data);
- this.url = url;
- }
-
- /**
- * Gets the URL reference for additional information about this protocol error.
- *
- * This URL typically points to protocol specification documentation or other resources
- * that provide context about the error condition.
- *
- * @return the reference URL, or null if not provided
+ * @param details additional error details as key-value pairs (defaults to empty map if null)
*/
- public @Nullable String getUrl() {
- return url;
+ public A2AProtocolError(Integer code, String message, @Nullable Map details) {
+ super(code, message, details);
}
}
diff --git a/spec/src/main/java/io/a2a/spec/ContentTypeNotSupportedError.java b/spec/src/main/java/io/a2a/spec/ContentTypeNotSupportedError.java
index cb657967c..b87addb87 100644
--- a/spec/src/main/java/io/a2a/spec/ContentTypeNotSupportedError.java
+++ b/spec/src/main/java/io/a2a/spec/ContentTypeNotSupportedError.java
@@ -1,8 +1,9 @@
package io.a2a.spec;
-import static io.a2a.spec.A2AErrorCodes.CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE;
import static io.a2a.util.Utils.defaultIfNull;
+import java.util.Map;
+
import org.jspecify.annotations.Nullable;
/**
@@ -48,12 +49,11 @@ public ContentTypeNotSupportedError() {
*
* @param code the error code
* @param message the error message
- * @param data additional error data
+ * @param details additional error details
*/
- public ContentTypeNotSupportedError(@Nullable Integer code, @Nullable String message, @Nullable Object data) {
- super(defaultIfNull(code, CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE),
+ public ContentTypeNotSupportedError(@Nullable Integer code, @Nullable String message, @Nullable Map details) {
+ super(defaultIfNull(code, A2AErrorCodes.CONTENT_TYPE_NOT_SUPPORTED.code()),
defaultIfNull(message, "Incompatible content types"),
- data,
- "https://a2a-protocol.org/errors/content-type-not-supported");
+ details);
}
}
diff --git a/spec/src/main/java/io/a2a/spec/ExtendedAgentCardNotConfiguredError.java b/spec/src/main/java/io/a2a/spec/ExtendedAgentCardNotConfiguredError.java
index 5b961e2bf..941788abd 100644
--- a/spec/src/main/java/io/a2a/spec/ExtendedAgentCardNotConfiguredError.java
+++ b/spec/src/main/java/io/a2a/spec/ExtendedAgentCardNotConfiguredError.java
@@ -1,8 +1,9 @@
package io.a2a.spec;
-import static io.a2a.spec.A2AErrorCodes.EXTENDED_AGENT_CARD_NOT_CONFIGURED_ERROR_CODE;
import static io.a2a.util.Utils.defaultIfNull;
+import java.util.Map;
+
import org.jspecify.annotations.Nullable;
@@ -36,13 +37,12 @@ public class ExtendedAgentCardNotConfiguredError extends A2AProtocolError {
*
* @param code the error code
* @param message the error message
- * @param data additional error data
+ * @param details additional error details
*/
- public ExtendedAgentCardNotConfiguredError(@Nullable Integer code, @Nullable String message, @Nullable Object data) {
+ public ExtendedAgentCardNotConfiguredError(@Nullable Integer code, @Nullable String message, @Nullable Map details) {
super(
- defaultIfNull(code, EXTENDED_AGENT_CARD_NOT_CONFIGURED_ERROR_CODE),
+ defaultIfNull(code, A2AErrorCodes.EXTENDED_AGENT_CARD_NOT_CONFIGURED.code()),
defaultIfNull(message, "Extended Card not configured"),
- data,
- "https://a2a-protocol.org/errors/extended-agent-card-not-configured");
+ details);
}
}
diff --git a/spec/src/main/java/io/a2a/spec/ExtensionSupportRequiredError.java b/spec/src/main/java/io/a2a/spec/ExtensionSupportRequiredError.java
index abb11aa9f..fd7882202 100644
--- a/spec/src/main/java/io/a2a/spec/ExtensionSupportRequiredError.java
+++ b/spec/src/main/java/io/a2a/spec/ExtensionSupportRequiredError.java
@@ -1,8 +1,9 @@
package io.a2a.spec;
-import static io.a2a.spec.A2AErrorCodes.EXTENSION_SUPPORT_REQUIRED_ERROR;
import static io.a2a.util.Utils.defaultIfNull;
+import java.util.Map;
+
import org.jspecify.annotations.Nullable;
@@ -36,13 +37,12 @@ public class ExtensionSupportRequiredError extends A2AProtocolError {
*
* @param code the error code (defaults to -32008 if null)
* @param message the error message (defaults to standard message if null)
- * @param data additional error data (optional)
+ * @param details additional error details (optional)
*/
- public ExtensionSupportRequiredError(@Nullable Integer code, @Nullable String message, @Nullable Object data) {
+ public ExtensionSupportRequiredError(@Nullable Integer code, @Nullable String message, @Nullable Map details) {
super(
- defaultIfNull(code, EXTENSION_SUPPORT_REQUIRED_ERROR),
+ defaultIfNull(code, A2AErrorCodes.EXTENSION_SUPPORT_REQUIRED.code()),
defaultIfNull(message, "Extension support required but not declared"),
- data,
- "https://a2a-protocol.org/errors/extension-support-required");
+ details);
}
}
diff --git a/spec/src/main/java/io/a2a/spec/InternalError.java b/spec/src/main/java/io/a2a/spec/InternalError.java
index 5d9793972..d6809f314 100644
--- a/spec/src/main/java/io/a2a/spec/InternalError.java
+++ b/spec/src/main/java/io/a2a/spec/InternalError.java
@@ -1,8 +1,9 @@
package io.a2a.spec;
-import static io.a2a.spec.A2AErrorCodes.INTERNAL_ERROR_CODE;
import static io.a2a.util.Utils.defaultIfNull;
+import java.util.Map;
+
import org.jspecify.annotations.Nullable;
@@ -34,13 +35,13 @@ public class InternalError extends A2AError {
*
* @param code the error code
* @param message the error message
- * @param data additional error data
+ * @param details additional error details
*/
- public InternalError(@Nullable Integer code, @Nullable String message, @Nullable Object data) {
+ public InternalError(@Nullable Integer code, @Nullable String message, @Nullable Map details) {
super(
- defaultIfNull(code, INTERNAL_ERROR_CODE),
+ defaultIfNull(code, A2AErrorCodes.INTERNAL.code()),
defaultIfNull(message, "Internal Error"),
- data);
+ details);
}
/**
diff --git a/spec/src/main/java/io/a2a/spec/InvalidAgentResponseError.java b/spec/src/main/java/io/a2a/spec/InvalidAgentResponseError.java
index d6d8508d9..c6c2c2cc2 100644
--- a/spec/src/main/java/io/a2a/spec/InvalidAgentResponseError.java
+++ b/spec/src/main/java/io/a2a/spec/InvalidAgentResponseError.java
@@ -1,8 +1,9 @@
package io.a2a.spec;
-import static io.a2a.spec.A2AErrorCodes.INVALID_AGENT_RESPONSE_ERROR_CODE;
import static io.a2a.util.Utils.defaultIfNull;
+import java.util.Map;
+
import org.jspecify.annotations.Nullable;
@@ -44,13 +45,12 @@ public class InvalidAgentResponseError extends A2AProtocolError {
*
* @param code the error code
* @param message the error message
- * @param data additional error data
+ * @param details additional error details
*/
- public InvalidAgentResponseError(@Nullable Integer code, @Nullable String message, @Nullable Object data) {
+ public InvalidAgentResponseError(@Nullable Integer code, @Nullable String message, @Nullable Map details) {
super(
- defaultIfNull(code, INVALID_AGENT_RESPONSE_ERROR_CODE),
+ defaultIfNull(code, A2AErrorCodes.INVALID_AGENT_RESPONSE.code()),
defaultIfNull(message, "Invalid agent response"),
- data,
- "https://a2a-protocol.org/errors/invalid-agent-response");
+ details);
}
}
diff --git a/spec/src/main/java/io/a2a/spec/InvalidParamsError.java b/spec/src/main/java/io/a2a/spec/InvalidParamsError.java
index a56f1ac91..6c6751d41 100644
--- a/spec/src/main/java/io/a2a/spec/InvalidParamsError.java
+++ b/spec/src/main/java/io/a2a/spec/InvalidParamsError.java
@@ -1,8 +1,9 @@
package io.a2a.spec;
-import static io.a2a.spec.A2AErrorCodes.INVALID_PARAMS_ERROR_CODE;
import static io.a2a.util.Utils.defaultIfNull;
+import java.util.Map;
+
import org.jspecify.annotations.Nullable;
/**
@@ -37,13 +38,13 @@ public class InvalidParamsError extends A2AError {
*
* @param code the error code
* @param message the error message
- * @param data additional error data
+ * @param details additional error details
*/
- public InvalidParamsError(@Nullable Integer code, @Nullable String message, @Nullable Object data) {
+ public InvalidParamsError(@Nullable Integer code, @Nullable String message, @Nullable Map details) {
super(
- defaultIfNull(code, INVALID_PARAMS_ERROR_CODE),
+ defaultIfNull(code, A2AErrorCodes.INVALID_PARAMS.code()),
defaultIfNull(message, "Invalid parameters"),
- data);
+ details);
}
/**
diff --git a/spec/src/main/java/io/a2a/spec/InvalidRequestError.java b/spec/src/main/java/io/a2a/spec/InvalidRequestError.java
index 512c21b59..151f0631f 100644
--- a/spec/src/main/java/io/a2a/spec/InvalidRequestError.java
+++ b/spec/src/main/java/io/a2a/spec/InvalidRequestError.java
@@ -1,8 +1,9 @@
package io.a2a.spec;
-import static io.a2a.spec.A2AErrorCodes.INVALID_REQUEST_ERROR_CODE;
import static io.a2a.util.Utils.defaultIfNull;
+import java.util.Map;
+
import org.jspecify.annotations.Nullable;
@@ -45,13 +46,13 @@ public InvalidRequestError() {
*
* @param code the error code
* @param message the error message
- * @param data additional error data
+ * @param details additional error details
*/
- public InvalidRequestError(@Nullable Integer code, @Nullable String message, @Nullable Object data) {
+ public InvalidRequestError(@Nullable Integer code, @Nullable String message, @Nullable Map details) {
super(
- defaultIfNull(code, INVALID_REQUEST_ERROR_CODE),
+ defaultIfNull(code, A2AErrorCodes.INVALID_REQUEST.code()),
defaultIfNull(message, "Request payload validation error"),
- data);
+ details);
}
/**
diff --git a/spec/src/main/java/io/a2a/spec/JSONParseError.java b/spec/src/main/java/io/a2a/spec/JSONParseError.java
index 72e4aaf02..5349add2d 100644
--- a/spec/src/main/java/io/a2a/spec/JSONParseError.java
+++ b/spec/src/main/java/io/a2a/spec/JSONParseError.java
@@ -1,8 +1,9 @@
package io.a2a.spec;
-import static io.a2a.spec.A2AErrorCodes.JSON_PARSE_ERROR_CODE;
import static io.a2a.util.Utils.defaultIfNull;
+import java.util.Map;
+
import org.jspecify.annotations.Nullable;
/**
@@ -51,12 +52,12 @@ public JSONParseError(String message) {
*
* @param code the error code
* @param message the error message
- * @param data additional error data
+ * @param details additional error details
*/
- public JSONParseError(@Nullable Integer code, @Nullable String message, @Nullable Object data) {
+ public JSONParseError(@Nullable Integer code, @Nullable String message, @Nullable Map details) {
super(
- defaultIfNull(code, JSON_PARSE_ERROR_CODE),
+ defaultIfNull(code, A2AErrorCodes.JSON_PARSE.code()),
defaultIfNull(message, "Invalid JSON payload"),
- data);
+ details);
}
}
diff --git a/spec/src/main/java/io/a2a/spec/MethodNotFoundError.java b/spec/src/main/java/io/a2a/spec/MethodNotFoundError.java
index c69aaa572..b97f5acae 100644
--- a/spec/src/main/java/io/a2a/spec/MethodNotFoundError.java
+++ b/spec/src/main/java/io/a2a/spec/MethodNotFoundError.java
@@ -1,8 +1,9 @@
package io.a2a.spec;
-import static io.a2a.spec.A2AErrorCodes.METHOD_NOT_FOUND_ERROR_CODE;
import static io.a2a.util.Utils.defaultIfNull;
+import java.util.Map;
+
import org.jspecify.annotations.Nullable;
/**
@@ -29,19 +30,19 @@ public class MethodNotFoundError extends A2AError {
*
* @param code the error code (defaults to -32601 if null)
* @param message the error message (defaults to "Method not found" if null)
- * @param data additional error data (optional)
+ * @param details additional error details (optional)
*/
- public MethodNotFoundError(@Nullable Integer code, @Nullable String message, @Nullable Object data) {
+ public MethodNotFoundError(@Nullable Integer code, @Nullable String message, @Nullable Map details) {
super(
- defaultIfNull(code, METHOD_NOT_FOUND_ERROR_CODE),
+ defaultIfNull(code, A2AErrorCodes.METHOD_NOT_FOUND.code()),
defaultIfNull(message, "Method not found"),
- data);
+ details);
}
/**
* Constructs error with default message.
*/
public MethodNotFoundError() {
- this(METHOD_NOT_FOUND_ERROR_CODE, null, null);
+ this(A2AErrorCodes.METHOD_NOT_FOUND.code(), null, null);
}
}
diff --git a/spec/src/main/java/io/a2a/spec/PushNotificationNotSupportedError.java b/spec/src/main/java/io/a2a/spec/PushNotificationNotSupportedError.java
index c84f4d79f..e167bb64a 100644
--- a/spec/src/main/java/io/a2a/spec/PushNotificationNotSupportedError.java
+++ b/spec/src/main/java/io/a2a/spec/PushNotificationNotSupportedError.java
@@ -1,8 +1,9 @@
package io.a2a.spec;
-import static io.a2a.spec.A2AErrorCodes.PUSH_NOTIFICATION_NOT_SUPPORTED_ERROR_CODE;
import static io.a2a.util.Utils.defaultIfNull;
+import java.util.Map;
+
import org.jspecify.annotations.Nullable;
/**
@@ -42,13 +43,12 @@ public PushNotificationNotSupportedError() {
*
* @param code the error code (defaults to -32003 if null)
* @param message the error message (defaults to "Push Notification is not supported" if null)
- * @param data additional error data (optional)
+ * @param details additional error details (optional)
*/
- public PushNotificationNotSupportedError(@Nullable Integer code, @Nullable String message, @Nullable Object data) {
+ public PushNotificationNotSupportedError(@Nullable Integer code, @Nullable String message, @Nullable Map details) {
super(
- defaultIfNull(code, PUSH_NOTIFICATION_NOT_SUPPORTED_ERROR_CODE),
+ defaultIfNull(code, A2AErrorCodes.PUSH_NOTIFICATION_NOT_SUPPORTED.code()),
defaultIfNull(message, "Push Notification is not supported"),
- data,
- "https://a2a-protocol.org/errors/push-notification-not-supported");
+ details);
}
}
diff --git a/spec/src/main/java/io/a2a/spec/TaskNotCancelableError.java b/spec/src/main/java/io/a2a/spec/TaskNotCancelableError.java
index 83e19ec54..49d86fa52 100644
--- a/spec/src/main/java/io/a2a/spec/TaskNotCancelableError.java
+++ b/spec/src/main/java/io/a2a/spec/TaskNotCancelableError.java
@@ -1,8 +1,9 @@
package io.a2a.spec;
-import static io.a2a.spec.A2AErrorCodes.TASK_NOT_CANCELABLE_ERROR_CODE;
import static io.a2a.util.Utils.defaultIfNull;
+import java.util.Map;
+
import org.jspecify.annotations.Nullable;
/**
@@ -45,14 +46,13 @@ public TaskNotCancelableError() {
*
* @param code the error code (defaults to -32002 if null)
* @param message the error message (defaults to "Task cannot be canceled" if null)
- * @param data additional error data (optional)
+ * @param details additional error details (optional)
*/
- public TaskNotCancelableError(@Nullable Integer code, @Nullable String message, @Nullable Object data) {
+ public TaskNotCancelableError(@Nullable Integer code, @Nullable String message, @Nullable Map details) {
super(
- defaultIfNull(code, TASK_NOT_CANCELABLE_ERROR_CODE),
+ defaultIfNull(code, A2AErrorCodes.TASK_NOT_CANCELABLE.code()),
defaultIfNull(message, "Task cannot be canceled"),
- data,
- "https://a2a-protocol.org/errors/task-not-cancelable");
+ details);
}
/**
diff --git a/spec/src/main/java/io/a2a/spec/TaskNotFoundError.java b/spec/src/main/java/io/a2a/spec/TaskNotFoundError.java
index 523d44f40..817175f84 100644
--- a/spec/src/main/java/io/a2a/spec/TaskNotFoundError.java
+++ b/spec/src/main/java/io/a2a/spec/TaskNotFoundError.java
@@ -1,8 +1,9 @@
package io.a2a.spec;
-import static io.a2a.spec.A2AErrorCodes.TASK_NOT_FOUND_ERROR_CODE;
import static io.a2a.util.Utils.defaultIfNull;
+import java.util.Map;
+
import org.jspecify.annotations.Nullable;
/**
@@ -45,13 +46,12 @@ public TaskNotFoundError() {
* Constructs error with all parameters.
*
* @param message the error message (defaults to "Task not found" if null)
- * @param data additional error data (optional)
+ * @param details additional error details (optional)
*/
- public TaskNotFoundError(@Nullable String message, @Nullable Object data) {
+ public TaskNotFoundError(@Nullable String message, @Nullable Map details) {
super(
- TASK_NOT_FOUND_ERROR_CODE,
+ A2AErrorCodes.TASK_NOT_FOUND.code(),
defaultIfNull(message, "Task not found"),
- data,
- "https://a2a-protocol.org/errors/task-not-found");
+ details);
}
}
diff --git a/spec/src/main/java/io/a2a/spec/UnsupportedOperationError.java b/spec/src/main/java/io/a2a/spec/UnsupportedOperationError.java
index 65bdbed9a..a0ced6f74 100644
--- a/spec/src/main/java/io/a2a/spec/UnsupportedOperationError.java
+++ b/spec/src/main/java/io/a2a/spec/UnsupportedOperationError.java
@@ -1,8 +1,9 @@
package io.a2a.spec;
-import static io.a2a.spec.A2AErrorCodes.UNSUPPORTED_OPERATION_ERROR_CODE;
import static io.a2a.util.Utils.defaultIfNull;
+import java.util.Map;
+
import org.jspecify.annotations.Nullable;
/**
@@ -40,14 +41,13 @@ public class UnsupportedOperationError extends A2AProtocolError {
*
* @param code the error code (defaults to -32004 if null)
* @param message the error message (defaults to "This operation is not supported" if null)
- * @param data additional error data (optional)
+ * @param details additional error details (optional)
*/
- public UnsupportedOperationError(@Nullable Integer code, @Nullable String message, @Nullable Object data) {
+ public UnsupportedOperationError(@Nullable Integer code, @Nullable String message, @Nullable Map details) {
super(
- defaultIfNull(code, UNSUPPORTED_OPERATION_ERROR_CODE),
+ defaultIfNull(code, A2AErrorCodes.UNSUPPORTED_OPERATION.code()),
defaultIfNull(message, "This operation is not supported"),
- data,
- "https://a2a-protocol.org/errors/unsupported-operation");
+ details);
}
/**
diff --git a/spec/src/main/java/io/a2a/spec/VersionNotSupportedError.java b/spec/src/main/java/io/a2a/spec/VersionNotSupportedError.java
index f4caf5e77..41dbed471 100644
--- a/spec/src/main/java/io/a2a/spec/VersionNotSupportedError.java
+++ b/spec/src/main/java/io/a2a/spec/VersionNotSupportedError.java
@@ -1,8 +1,9 @@
package io.a2a.spec;
-import static io.a2a.spec.A2AErrorCodes.VERSION_NOT_SUPPORTED_ERROR_CODE;
import static io.a2a.util.Utils.defaultIfNull;
+import java.util.Map;
+
import org.jspecify.annotations.Nullable;
@@ -35,13 +36,12 @@ public class VersionNotSupportedError extends A2AProtocolError {
*
* @param code the error code (defaults to -32009 if null)
* @param message the error message (defaults to standard message if null)
- * @param data additional error data (optional)
+ * @param details additional error details (optional)
*/
- public VersionNotSupportedError(@Nullable Integer code, @Nullable String message, @Nullable Object data) {
+ public VersionNotSupportedError(@Nullable Integer code, @Nullable String message, @Nullable Map details) {
super(
- defaultIfNull(code, VERSION_NOT_SUPPORTED_ERROR_CODE),
+ defaultIfNull(code, A2AErrorCodes.VERSION_NOT_SUPPORTED.code()),
defaultIfNull(message, "Protocol version not supported"),
- data,
- "https://a2a-protocol.org/errors/version-not-supported");
+ details);
}
}
diff --git a/transport/grpc/src/main/java/io/a2a/transport/grpc/handler/GrpcHandler.java b/transport/grpc/src/main/java/io/a2a/transport/grpc/handler/GrpcHandler.java
index 4eb8c7223..fe21c5ce3 100644
--- a/transport/grpc/src/main/java/io/a2a/transport/grpc/handler/GrpcHandler.java
+++ b/transport/grpc/src/main/java/io/a2a/transport/grpc/handler/GrpcHandler.java
@@ -21,6 +21,7 @@
import io.a2a.common.A2AErrorMessages;
import io.a2a.grpc.A2AServiceGrpc;
import io.a2a.grpc.StreamResponse;
+import io.a2a.jsonrpc.common.json.JsonUtil;
import io.a2a.jsonrpc.common.wrappers.ListTasksResult;
import io.a2a.server.AgentCardValidator;
import io.a2a.server.ServerCallContext;
@@ -56,12 +57,14 @@
import io.a2a.spec.TaskPushNotificationConfig;
import io.a2a.spec.TaskQueryParams;
import io.a2a.spec.TransportProtocol;
+import io.a2a.spec.A2AErrorCodes;
import io.a2a.spec.UnsupportedOperationError;
import io.a2a.spec.VersionNotSupportedError;
import io.a2a.transport.grpc.context.GrpcContextKeys;
import io.grpc.Context;
import io.grpc.Metadata;
import io.grpc.Status;
+import io.grpc.protobuf.StatusProto;
import io.grpc.stub.StreamObserver;
import org.jspecify.annotations.Nullable;
@@ -719,55 +722,26 @@ private ServerCallContext createCallContext(StreamObserver responseObserv
* @param error the A2A protocol error
*/
private void handleError(StreamObserver responseObserver, A2AError error) {
- Status status;
- String description;
- if (error instanceof InvalidRequestError) {
- status = Status.INVALID_ARGUMENT;
- description = "InvalidRequestError: " + error.getMessage();
- } else if (error instanceof MethodNotFoundError) {
- status = Status.NOT_FOUND;
- description = "MethodNotFoundError: " + error.getMessage();
- } else if (error instanceof InvalidParamsError) {
- status = Status.INVALID_ARGUMENT;
- description = "InvalidParamsError: " + error.getMessage();
- } else if (error instanceof InternalError) {
- status = Status.INTERNAL;
- description = "InternalError: " + error.getMessage();
- } else if (error instanceof TaskNotFoundError) {
- status = Status.NOT_FOUND;
- description = "TaskNotFoundError: " + error.getMessage();
- } else if (error instanceof TaskNotCancelableError) {
- status = Status.FAILED_PRECONDITION;
- description = "TaskNotCancelableError: " + error.getMessage();
- } else if (error instanceof PushNotificationNotSupportedError) {
- status = Status.UNIMPLEMENTED;
- description = "PushNotificationNotSupportedError: " + error.getMessage();
- } else if (error instanceof UnsupportedOperationError) {
- status = Status.UNIMPLEMENTED;
- description = "UnsupportedOperationError: " + error.getMessage();
- } else if (error instanceof JSONParseError) {
- status = Status.INTERNAL;
- description = "JSONParseError: " + error.getMessage();
- } else if (error instanceof ContentTypeNotSupportedError) {
- status = Status.INVALID_ARGUMENT;
- description = "ContentTypeNotSupportedError: " + error.getMessage();
- } else if (error instanceof InvalidAgentResponseError) {
- status = Status.INTERNAL;
- description = "InvalidAgentResponseError: " + error.getMessage();
- } else if (error instanceof ExtendedAgentCardNotConfiguredError) {
- status = Status.FAILED_PRECONDITION;
- description = "ExtendedCardNotConfiguredError: " + error.getMessage();
- } else if (error instanceof ExtensionSupportRequiredError) {
- status = Status.FAILED_PRECONDITION;
- description = "ExtensionSupportRequiredError: " + error.getMessage();
- } else if (error instanceof VersionNotSupportedError) {
- status = Status.UNIMPLEMENTED;
- description = "VersionNotSupportedError: " + error.getMessage();
- } else {
- status = Status.UNKNOWN;
- description = "Unknown error type: " + error.getMessage();
+ A2AErrorCodes errorCode = A2AErrorCodes.fromCode(error.getCode());
+ String grpcStatusName = errorCode != null ? errorCode.grpcStatus() : "UNKNOWN";
+ String reason = errorCode != null ? errorCode.name() : "UNKNOWN";
+ int grpcCode = Status.Code.valueOf(grpcStatusName).value();
+
+ com.google.rpc.ErrorInfo.Builder errorInfoBuilder = com.google.rpc.ErrorInfo.newBuilder()
+ .setReason(reason)
+ .setDomain("a2a-protocol.org");
+ if (!error.getDetails().isEmpty()) {
+ error.getDetails().forEach((k, v) ->
+ errorInfoBuilder.putMetadata(k, v instanceof String s ? s : JsonUtil.OBJECT_MAPPER.toJson(v)));
}
- responseObserver.onError(status.withDescription(description).asRuntimeException());
+
+ com.google.rpc.Status rpcStatus = com.google.rpc.Status.newBuilder()
+ .setCode(grpcCode)
+ .setMessage(error.getMessage() != null ? error.getMessage() : "")
+ .addDetails(com.google.protobuf.Any.pack(errorInfoBuilder.build()))
+ .build();
+
+ responseObserver.onError(StatusProto.toStatusRuntimeException(rpcStatus));
}
/**
diff --git a/transport/grpc/src/test/java/io/a2a/transport/grpc/handler/GrpcHandlerTest.java b/transport/grpc/src/test/java/io/a2a/transport/grpc/handler/GrpcHandlerTest.java
index c0feac4d7..b26b58c53 100644
--- a/transport/grpc/src/test/java/io/a2a/transport/grpc/handler/GrpcHandlerTest.java
+++ b/transport/grpc/src/test/java/io/a2a/transport/grpc/handler/GrpcHandlerTest.java
@@ -1185,8 +1185,21 @@ private void sendStreamingMessageRequest(GrpcHandler handler, StreamObserver void assertGrpcError(StreamRecorder streamRecorder, Status.Code expectedStatusCode) {
Assertions.assertNotNull(streamRecorder.getError());
Assertions.assertInstanceOf(StatusRuntimeException.class, streamRecorder.getError());
- Assertions.assertEquals(expectedStatusCode, ((StatusRuntimeException) streamRecorder.getError()).getStatus().getCode());
+ StatusRuntimeException sre = (StatusRuntimeException) streamRecorder.getError();
+ Assertions.assertEquals(expectedStatusCode, sre.getStatus().getCode());
Assertions.assertTrue(streamRecorder.getValues().isEmpty());
+
+ // Verify ErrorInfo is present in status details
+ com.google.rpc.Status rpcStatus = io.grpc.protobuf.StatusProto.fromThrowable(sre);
+ Assertions.assertNotNull(rpcStatus, "rpc status should be present");
+ Assertions.assertFalse(rpcStatus.getDetailsList().isEmpty(), "details should not be empty");
+ try {
+ com.google.rpc.ErrorInfo errorInfo = rpcStatus.getDetails(0).unpack(com.google.rpc.ErrorInfo.class);
+ Assertions.assertEquals("a2a-protocol.org", errorInfo.getDomain());
+ Assertions.assertFalse(errorInfo.getReason().isEmpty(), "reason should not be empty");
+ } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+ Assertions.fail("Failed to unpack ErrorInfo: " + e.getMessage());
+ }
}
@Test
diff --git a/transport/rest/src/main/java/io/a2a/transport/rest/handler/RestHandler.java b/transport/rest/src/main/java/io/a2a/transport/rest/handler/RestHandler.java
index 8491d9216..364b87b77 100644
--- a/transport/rest/src/main/java/io/a2a/transport/rest/handler/RestHandler.java
+++ b/transport/rest/src/main/java/io/a2a/transport/rest/handler/RestHandler.java
@@ -3,12 +3,13 @@
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;
+import io.a2a.spec.A2AErrorCodes;
import java.time.Instant;
import java.time.format.DateTimeParseException;
import java.util.Arrays;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
@@ -41,31 +42,25 @@
import io.a2a.spec.AgentCard;
import io.a2a.spec.CancelTaskParams;
import io.a2a.spec.ExtendedAgentCardNotConfiguredError;
-import io.a2a.spec.ContentTypeNotSupportedError;
import io.a2a.spec.DeleteTaskPushNotificationConfigParams;
import io.a2a.spec.EventKind;
import io.a2a.spec.GetTaskPushNotificationConfigParams;
import io.a2a.spec.InternalError;
-import io.a2a.spec.InvalidAgentResponseError;
import io.a2a.spec.InvalidParamsError;
import io.a2a.spec.InvalidRequestError;
import io.a2a.spec.JSONParseError;
import io.a2a.spec.ListTaskPushNotificationConfigsParams;
import io.a2a.spec.ListTaskPushNotificationConfigsResult;
import io.a2a.spec.ListTasksParams;
-import io.a2a.spec.MethodNotFoundError;
import io.a2a.spec.PushNotificationNotSupportedError;
import io.a2a.spec.StreamingEventKind;
import io.a2a.spec.Task;
import io.a2a.spec.TaskIdParams;
-import io.a2a.spec.TaskNotCancelableError;
import io.a2a.spec.TaskNotFoundError;
import io.a2a.spec.TaskPushNotificationConfig;
import io.a2a.spec.TaskQueryParams;
import io.a2a.spec.TaskState;
import io.a2a.spec.UnsupportedOperationError;
-import io.a2a.spec.ExtensionSupportRequiredError;
-import io.a2a.spec.VersionNotSupportedError;
import mutiny.zero.ZeroPublisher;
import org.jspecify.annotations.Nullable;
@@ -647,7 +642,7 @@ private void validate(String json) {
try {
JsonParser.parseString(json);
} catch (JsonSyntaxException e) {
- throw new JSONParseError(JSON_PARSE_ERROR_CODE, "Failed to parse json", e.getMessage());
+ throw new JSONParseError(A2AErrorCodes.JSON_PARSE.code(), "Failed to parse json", null);
}
}
@@ -745,113 +740,17 @@ public void onComplete() {
}
/**
- * Maps A2A protocol errors to HTTP status codes.
- *
- *
- * This method ensures consistent HTTP status code mapping for all A2A errors:
- *
- * - 400 - Invalid request, JSON parse errors, missing extensions
- * - 404 - Method not found, task not found
- * - 409 - Task not cancelable (conflict)
- * - 415 - Unsupported content type
- * - 422 - Invalid parameters (unprocessable entity)
- * - 500 - Internal errors
- * - 501 - Not implemented (unsupported operations, version)
- * - 502 - Bad gateway (invalid agent response)
- *
+ * Maps A2A protocol errors to HTTP status codes using {@link A2AErrorCodes}.
*
* @param error the A2A error to map
* @return the corresponding HTTP status code
*/
private static int mapErrorToHttpStatus(A2AError error) {
- if (error instanceof InvalidRequestError || error instanceof JSONParseError) {
- return 400;
- }
- if (error instanceof InvalidParamsError) {
- return 422;
- }
- if (error instanceof MethodNotFoundError || error instanceof TaskNotFoundError) {
- return 404;
- }
- if (error instanceof TaskNotCancelableError) {
- return 409;
- }
- if (error instanceof PushNotificationNotSupportedError
- || error instanceof UnsupportedOperationError) {
- return 501;
- }
- if (error instanceof ContentTypeNotSupportedError) {
- return 415;
- }
- if (error instanceof InvalidAgentResponseError) {
- return 502;
- }
- if (error instanceof ExtendedAgentCardNotConfiguredError
- || error instanceof ExtensionSupportRequiredError
- || error instanceof VersionNotSupportedError) {
- return 400;
- }
- if (error instanceof InternalError) {
- return 500;
+ A2AErrorCodes errorCode = A2AErrorCodes.fromCode(error.getCode());
+ if (errorCode != null) {
+ return errorCode.httpCode();
}
- return 500;
- }
-
- /**
- * Maps A2A protocol errors to RFC 7807 Problem Details type URIs.
- *
- *
- * This method provides a unique URI for each A2A error type, which is used in the "type"
- * field of RFC 7807 error responses. For example:
- *
- * - {@link InvalidRequestError} -> "https://a2a-protocol.org/errors/invalid-request"
- * - {@link TaskNotFoundError} -> "https://a2a-protocol.org/errors/task-not-found"
- *
- *
- * @param error the A2A error to map
- * @return the corresponding RFC 7807 type URI
- */
- private static String mapErrorToURI(A2AError error) {
- if (error instanceof InvalidRequestError || error instanceof JSONParseError) {
- return "https://a2a-protocol.org/errors/invalid-request";
- }
- if (error instanceof InvalidParamsError) {
- return "https://a2a-protocol.org/errors/invalid-params";
- }
- if (error instanceof MethodNotFoundError) {
- return "https://a2a-protocol.org/errors/method-not-found";
- }
- if (error instanceof TaskNotFoundError) {
- return "https://a2a-protocol.org/errors/task-not-found";
- }
- if (error instanceof TaskNotCancelableError) {
- return "https://a2a-protocol.org/errors/task-not-cancelable";
- }
- if (error instanceof PushNotificationNotSupportedError) {
- return "https://a2a-protocol.org/errors/push-notification-not-supported";
- }
- if (error instanceof UnsupportedOperationError) {
- return "https://a2a-protocol.org/errors/unsupported-operation";
- }
- if (error instanceof VersionNotSupportedError) {
- return "https://a2a-protocol.org/errors/version-not-supported";
- }
- if (error instanceof ContentTypeNotSupportedError) {
- return "https://a2a-protocol.org/errors/content-type-not-supported";
- }
- if (error instanceof InvalidAgentResponseError) {
- return "https://a2a-protocol.org/errors/invalid-agent-response";
- }
- if (error instanceof ExtendedAgentCardNotConfiguredError) {
- return "https://a2a-protocol.org/errors/extended-agent-card-not-configured";
- }
- if (error instanceof ExtensionSupportRequiredError) {
- return "https://a2a-protocol.org/errors/extension-support-required";
- }
- if (error instanceof InternalError) {
- return "https://a2a-protocol.org/errors/internal-error";
- }
- return "https://a2a-protocol.org/errors/internal-error";
+ return A2AErrorCodes.INTERNAL.httpCode();
}
/**
@@ -1017,27 +916,46 @@ public Flow.Publisher getPublisher() {
}
}
+ private static final String ERROR_INFO_TYPE = "type.googleapis.com/google.rpc.ErrorInfo";
+ private static final String ERROR_DOMAIN = "a2a-protocol.org";
+
/**
- * Represents an HTTP error response containing A2A error details.
+ * Represents an HTTP error response containing A2A error details in the Google Cloud API error format.
+ *
+ * Produces JSON of the form:
+ *
{@code
+ * {
+ * "error": {
+ * "code": 404,
+ * "status": "NOT_FOUND",
+ * "message": "Task not found",
+ * "details": [
+ * {
+ * "@type": "type.googleapis.com/google.rpc.ErrorInfo",
+ * "reason": "TASK_NOT_FOUND",
+ * "domain": "a2a-protocol.org",
+ * "metadata": { ... }
+ * }
+ * ]
+ * }
+ * }
+ * }
*/
private static class HTTPRestErrorResponse {
- private final String title;
- private final String details;
- private final int status;
- private final String type;
+ private final ErrorBody error;
+ private HTTPRestErrorResponse(A2AError a2aError) {
+ A2AErrorCodes errorCode = A2AErrorCodes.fromCode(a2aError.getCode());
+ int httpCode = mapErrorToHttpStatus(a2aError);
+ String status = errorCode != null
+ ? errorCode.grpcStatus()
+ : A2AErrorCodes.INTERNAL.grpcStatus();
+ String reason = errorCode != null ? errorCode.name() : "INTERNAL";
+ String message = a2aError.getMessage() == null ? a2aError.getClass().getName() : a2aError.getMessage();
- /**
- * Creates an error response from an A2A error.
- *
- * @param jsonRpcError the A2A error
- */
- private HTTPRestErrorResponse(A2AError jsonRpcError) {
- this.title = jsonRpcError.getMessage() == null ? jsonRpcError.getClass().getName() : jsonRpcError.getMessage();
- this.details = jsonRpcError.getData() == null ? "" : jsonRpcError.getData().toString();
- this.type = mapErrorToURI(jsonRpcError);
- this.status = mapErrorToHttpStatus(jsonRpcError);
+ ErrorDetail detail = new ErrorDetail(ERROR_INFO_TYPE, reason, ERROR_DOMAIN, a2aError.getDetails());
+ this.error = new ErrorBody(httpCode, status, message, List.of(detail));
}
private String toJson() {
@@ -1045,14 +963,21 @@ private String toJson() {
return JsonUtil.toJson(this);
} catch (JsonProcessingException ex) {
log.log(Level.SEVERE, "Failed to serialize HTTPRestErrorResponse to JSON", ex);
- return "{\"title\":\"Internal Server Error\",\"details\":\"Failed to serialize error response.\",\"status\":500,\"type\":\"https://a2a-protocol.org/errors/internal-error\"}";
+ return "{\"error\":{\"code\":500,\"status\":\"INTERNAL\",\"message\":\"Internal Server Error\",\"details\":[]}}";
}
}
@Override
public String toString() {
- return "HTTPRestErrorResponse{" + "title=" + title + ", details=" + details + ", status=" + status + ", type=" + type + '}';
+ return "HTTPRestErrorResponse{error=" + error + '}';
}
+ private record ErrorBody(int code, String status, String message, List details) {}
+
+ private record ErrorDetail(
+ @com.google.gson.annotations.SerializedName("@type") String type,
+ String reason,
+ String domain,
+ Map metadata) {}
}
}
diff --git a/transport/rest/src/test/java/io/a2a/transport/rest/handler/RestHandlerTest.java b/transport/rest/src/test/java/io/a2a/transport/rest/handler/RestHandlerTest.java
index 738b027e7..d07f5a42c 100644
--- a/transport/rest/src/test/java/io/a2a/transport/rest/handler/RestHandlerTest.java
+++ b/transport/rest/src/test/java/io/a2a/transport/rest/handler/RestHandlerTest.java
@@ -56,7 +56,7 @@ public void testGetTaskNotFound() {
RestHandler.HTTPRestResponse response = handler.getTask(callContext, "", "nonexistent", 0);
assertProblemDetail(response, 404,
- "https://a2a-protocol.org/errors/task-not-found", "Task not found");
+ "TASK_NOT_FOUND", "Task not found");
}
@Test
@@ -66,7 +66,7 @@ public void testGetTaskNegativeHistoryLengthReturns422() {
RestHandler.HTTPRestResponse response = handler.getTask(callContext, "", MINIMAL_TASK.id(), -1);
assertProblemDetail(response, 422,
- "https://a2a-protocol.org/errors/invalid-params", "Invalid history length");
+ "INVALID_PARAMS", "Invalid history length");
}
@Test
@@ -90,7 +90,7 @@ public void testListTasksInvalidStatus() {
null, null, null);
assertProblemDetail(response, 422,
- "https://a2a-protocol.org/errors/invalid-params", "Invalid params");
+ "INVALID_PARAMS", "Invalid params");
}
@Test
@@ -132,7 +132,7 @@ public void testSendMessageInvalidBody() {
RestHandler.HTTPRestResponse response = handler.sendMessage(callContext, "", invalidBody);
assertProblemDetail(response, 400,
- "https://a2a-protocol.org/errors/invalid-request", "Failed to parse json");
+ "JSON_PARSE", "Failed to parse json");
}
@Test
@@ -157,10 +157,11 @@ public void testSendMessageWrongValueBody() {
Assertions.assertEquals(422, response.getStatusCode());
Assertions.assertEquals("application/problem+json", response.getContentType());
JsonObject body = JsonParser.parseString(response.getBody()).getAsJsonObject();
- Assertions.assertEquals(422, body.get("status").getAsInt());
- Assertions.assertEquals("https://a2a-protocol.org/errors/invalid-params", body.get("type").getAsString());
- Assertions.assertTrue(body.get("title").getAsString().startsWith("Failed to parse request body:"),
- "title should indicate parse failure: " + body.get("title").getAsString());
+ JsonObject error = body.getAsJsonObject("error");
+ Assertions.assertEquals(422, error.get("code").getAsInt());
+ Assertions.assertEquals("INVALID_PARAMS", error.getAsJsonArray("details").get(0).getAsJsonObject().get("reason").getAsString());
+ Assertions.assertTrue(error.get("message").getAsString().startsWith("Failed to parse request body:"),
+ "message should indicate parse failure: " + error.get("message").getAsString());
}
@Test
@@ -170,7 +171,7 @@ public void testSendMessageEmptyBody() {
RestHandler.HTTPRestResponse response = handler.sendMessage(callContext, "", "");
assertProblemDetail(response, 400,
- "https://a2a-protocol.org/errors/invalid-request", "Request body is required");
+ "INVALID_REQUEST", "Request body is required");
}
@Test
@@ -202,7 +203,7 @@ public void testCancelTaskNotFound() {
RestHandler.HTTPRestResponse response = handler.cancelTask(callContext, "", requestBody, "nonexistent");
assertProblemDetail(response, 404,
- "https://a2a-protocol.org/errors/task-not-found", "Task not found");
+ "TASK_NOT_FOUND", "Task not found");
}
@Test
@@ -347,7 +348,7 @@ public void testSendStreamingMessageNotSupported() {
RestHandler.HTTPRestResponse response = handler.sendStreamingMessage(callContext, "", requestBody);
assertProblemDetail(response, 400,
- "https://a2a-protocol.org/errors/invalid-request",
+ "INVALID_REQUEST",
"Streaming is not supported by the agent");
}
@@ -388,8 +389,8 @@ public void testPushNotificationConfigNotSupported() {
RestHandler.HTTPRestResponse response = handler.createTaskPushNotificationConfiguration(callContext, "", requestBody, MINIMAL_TASK.id());
- assertProblemDetail(response, 501,
- "https://a2a-protocol.org/errors/push-notification-not-supported",
+ assertProblemDetail(response, 400,
+ "PUSH_NOTIFICATION_NOT_SUPPORTED",
"Push Notification is not supported");
}
@@ -577,7 +578,7 @@ public void testExtensionSupportRequiredErrorOnSendMessage() {
RestHandler.HTTPRestResponse response = handler.sendMessage(callContext, "", requestBody);
assertProblemDetail(response, 400,
- "https://a2a-protocol.org/errors/extension-support-required",
+ "EXTENSION_SUPPORT_REQUIRED",
"Required extension 'https://example.com/test-extension' was not requested by the client");
}
@@ -642,11 +643,17 @@ public void onSubscribe(Flow.Subscription subscription) {
@Override
public void onNext(String item) {
- JsonObject error = JsonParser.parseString(item).getAsJsonObject();
- if ("https://a2a-protocol.org/errors/extension-support-required".equals(
- error.has("type") ? error.get("type").getAsString() : null) &&
- item.contains("https://example.com/streaming-extension")) {
- errorFound.set(true);
+ JsonObject body = JsonParser.parseString(item).getAsJsonObject();
+ if (body.has("error")) {
+ JsonObject error = body.getAsJsonObject("error");
+ var details = error.has("details") ? error.getAsJsonArray("details") : null;
+ if (details != null && !details.isEmpty()) {
+ String reason = details.get(0).getAsJsonObject().get("reason").getAsString();
+ if ("EXTENSION_SUPPORT_REQUIRED".equals(reason) &&
+ item.contains("https://example.com/streaming-extension")) {
+ errorFound.set(true);
+ }
+ }
}
latch.countDown();
}
@@ -778,7 +785,7 @@ public void testVersionNotSupportedErrorOnSendMessage() {
RestHandler.HTTPRestResponse response = handler.sendMessage(contextWithVersion, "", requestBody);
assertProblemDetail(response, 400,
- "https://a2a-protocol.org/errors/version-not-supported",
+ "VERSION_NOT_SUPPORTED",
"Protocol version '2.0' is not supported. Supported versions: [1.0]");
}
@@ -845,11 +852,17 @@ public void onSubscribe(Flow.Subscription subscription) {
@Override
public void onNext(String item) {
- JsonObject error = JsonParser.parseString(item).getAsJsonObject();
- if ("https://a2a-protocol.org/errors/version-not-supported".equals(
- error.has("type") ? error.get("type").getAsString() : null) &&
- error.has("title") && error.get("title").getAsString().contains("2.0")) {
- errorFound.set(true);
+ JsonObject body = JsonParser.parseString(item).getAsJsonObject();
+ if (body.has("error")) {
+ JsonObject error = body.getAsJsonObject("error");
+ var details = error.has("details") ? error.getAsJsonArray("details") : null;
+ if (details != null && !details.isEmpty()) {
+ String reason = details.get(0).getAsJsonObject().get("reason").getAsString();
+ if ("VERSION_NOT_SUPPORTED".equals(reason) &&
+ error.has("message") && error.get("message").getAsString().contains("2.0")) {
+ errorFound.set(true);
+ }
+ }
}
}
@@ -984,7 +997,7 @@ public void testListTasksNegativeTimestampReturns422() {
null, "-1", null);
assertProblemDetail(response, 422,
- "https://a2a-protocol.org/errors/invalid-params", "Invalid params");
+ "INVALID_PARAMS", "Invalid params");
}
@Test
@@ -1047,13 +1060,21 @@ public void testListTasksEmptyResultIncludesAllFields() {
}
private static void assertProblemDetail(RestHandler.HTTPRestResponse response,
- int expectedStatus, String expectedType, String expectedTitle) {
+ int expectedStatus, String expectedReason, String expectedMessage) {
Assertions.assertEquals(expectedStatus, response.getStatusCode());
Assertions.assertEquals("application/problem+json", response.getContentType());
JsonObject body = JsonParser.parseString(response.getBody()).getAsJsonObject();
- Assertions.assertEquals(expectedStatus, body.get("status").getAsInt(), "status field mismatch");
- Assertions.assertEquals(expectedType, body.get("type").getAsString(), "type field mismatch");
- Assertions.assertEquals(expectedTitle, body.get("title").getAsString(), "title field mismatch");
- Assertions.assertTrue(body.has("details"), "details field should be present");
+ Assertions.assertTrue(body.has("error"), "error wrapper should be present");
+ JsonObject error = body.getAsJsonObject("error");
+ Assertions.assertEquals(expectedStatus, error.get("code").getAsInt(), "code field mismatch");
+ Assertions.assertEquals(expectedMessage, error.get("message").getAsString(), "message field mismatch");
+ Assertions.assertTrue(error.has("status"), "status field should be present");
+ Assertions.assertTrue(error.has("details"), "details field should be present");
+ var details = error.getAsJsonArray("details");
+ Assertions.assertFalse(details.isEmpty(), "details array should not be empty");
+ JsonObject detail = details.get(0).getAsJsonObject();
+ Assertions.assertEquals("type.googleapis.com/google.rpc.ErrorInfo", detail.get("@type").getAsString(), "@type field mismatch");
+ Assertions.assertEquals(expectedReason, detail.get("reason").getAsString(), "reason field mismatch");
+ Assertions.assertEquals("a2a-protocol.org", detail.get("domain").getAsString(), "domain field mismatch");
}
}