From 0fe881ec048dd33f9b7082a92bbdddb001e99881 Mon Sep 17 00:00:00 2001 From: Kabir Date: Thu, 12 Mar 2026 18:02:50 +0000 Subject: [PATCH 1/2] fix: Include google.rpc.ErrorInfo in gRPC error status details Per specification requirement GRPC-ERR-001 (section 10.6), A2A-specific errors must include a google.rpc.ErrorInfo message in the status.details array with: - reason: A2A error type in UPPER_SNAKE_CASE without "Error" suffix - domain: "a2a-protocol.org" - metadata: Optional map of additional error context Modified GrpcHandler.handleError() to: 1. Create google.rpc.ErrorInfo with appropriate reason and domain 2. Build google.rpc.Status containing the ErrorInfo in details 3. Include Status in grpc-status-details-bin trailing metadata This ensures gRPC error responses comply with the A2A specification by providing structured error information that clients can programmatically inspect and handle. Fixes #731 Co-Authored-By: Claude Sonnet 4.5 --- .../transport/grpc/handler/GrpcHandler.java | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) 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 4d652377c..a086fcef5 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 @@ -721,53 +721,92 @@ private ServerCallContext createCallContext(StreamObserver responseObserv private void handleError(StreamObserver responseObserver, A2AError error) { Status status; String description; + String errorReason; + if (error instanceof InvalidRequestError) { status = Status.INVALID_ARGUMENT; description = "InvalidRequestError: " + error.getMessage(); + errorReason = "INVALID_REQUEST"; } else if (error instanceof MethodNotFoundError) { status = Status.NOT_FOUND; description = "MethodNotFoundError: " + error.getMessage(); + errorReason = "METHOD_NOT_FOUND"; } else if (error instanceof InvalidParamsError) { status = Status.INVALID_ARGUMENT; description = "InvalidParamsError: " + error.getMessage(); + errorReason = "INVALID_PARAMS"; } else if (error instanceof InternalError) { status = Status.INTERNAL; description = "InternalError: " + error.getMessage(); + errorReason = "INTERNAL"; } else if (error instanceof TaskNotFoundError) { status = Status.NOT_FOUND; description = "TaskNotFoundError: " + error.getMessage(); + errorReason = "TASK_NOT_FOUND"; } else if (error instanceof TaskNotCancelableError) { status = Status.FAILED_PRECONDITION; description = "TaskNotCancelableError: " + error.getMessage(); + errorReason = "TASK_NOT_CANCELABLE"; } else if (error instanceof PushNotificationNotSupportedError) { status = Status.UNIMPLEMENTED; description = "PushNotificationNotSupportedError: " + error.getMessage(); + errorReason = "PUSH_NOTIFICATION_NOT_SUPPORTED"; } else if (error instanceof UnsupportedOperationError) { status = Status.UNIMPLEMENTED; description = "UnsupportedOperationError: " + error.getMessage(); + errorReason = "UNSUPPORTED_OPERATION"; } else if (error instanceof JSONParseError) { status = Status.INTERNAL; description = "JSONParseError: " + error.getMessage(); + errorReason = "JSON_PARSE"; } else if (error instanceof ContentTypeNotSupportedError) { status = Status.INVALID_ARGUMENT; description = "ContentTypeNotSupportedError: " + error.getMessage(); + errorReason = "CONTENT_TYPE_NOT_SUPPORTED"; } else if (error instanceof InvalidAgentResponseError) { status = Status.INTERNAL; description = "InvalidAgentResponseError: " + error.getMessage(); + errorReason = "INVALID_AGENT_RESPONSE"; } else if (error instanceof ExtendedAgentCardNotConfiguredError) { status = Status.FAILED_PRECONDITION; description = "ExtendedCardNotConfiguredError: " + error.getMessage(); + errorReason = "EXTENDED_AGENT_CARD_NOT_CONFIGURED"; } else if (error instanceof ExtensionSupportRequiredError) { status = Status.FAILED_PRECONDITION; description = "ExtensionSupportRequiredError: " + error.getMessage(); + errorReason = "EXTENSION_SUPPORT_REQUIRED"; } else if (error instanceof VersionNotSupportedError) { status = Status.UNIMPLEMENTED; description = "VersionNotSupportedError: " + error.getMessage(); + errorReason = "VERSION_NOT_SUPPORTED"; } else { status = Status.UNKNOWN; description = "Unknown error type: " + error.getMessage(); + errorReason = "UNKNOWN"; } - responseObserver.onError(status.withDescription(description).asRuntimeException()); + + // Create ErrorInfo per GRPC-ERR-001 specification requirement + com.google.rpc.ErrorInfo errorInfo = com.google.rpc.ErrorInfo.newBuilder() + .setReason(errorReason) + .setDomain("a2a-protocol.org") + .build(); + + // Create Status with ErrorInfo in details + com.google.rpc.Status rpcStatus = com.google.rpc.Status.newBuilder() + .setCode(status.getCode().value()) + .setMessage(description) + .addDetails(com.google.protobuf.Any.pack(errorInfo)) + .build(); + + // Create metadata with grpc-status-details-bin + io.grpc.Metadata trailers = new io.grpc.Metadata(); + io.grpc.Metadata.Key statusKey = + io.grpc.Metadata.Key.of("grpc-status-details-bin", + io.grpc.protobuf.ProtoUtils.metadataMarshaller(com.google.rpc.Status.getDefaultInstance())); + trailers.put(statusKey, rpcStatus); + + // Send error with ErrorInfo in metadata + responseObserver.onError(status.withDescription(description).asRuntimeException(trailers)); } /** From 24d66c4be001a622d9afcc68bf3a1db3054a90b6 Mon Sep 17 00:00:00 2001 From: Kabir Khan Date: Thu, 12 Mar 2026 22:24:04 +0000 Subject: [PATCH 2/2] Incorporate fixes --- .../io/a2a/transport/grpc/handler/GrpcHandler.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) 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 a086fcef5..eef71652e 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 @@ -158,6 +158,10 @@ public abstract class GrpcHandler extends A2AServiceGrpc.A2AServiceImplBase { private static final Logger LOGGER = Logger.getLogger(GrpcHandler.class.getName()); + private static final io.grpc.Metadata.Key GRPC_STATUS_DETAILS_KEY = + io.grpc.Metadata.Key.of("grpc-status-details-bin", + io.grpc.protobuf.ProtoUtils.metadataMarshaller(com.google.rpc.Status.getDefaultInstance())); + /** * Constructs a new GrpcHandler. */ @@ -769,7 +773,7 @@ private void handleError(StreamObserver responseObserver, A2AError error) errorReason = "INVALID_AGENT_RESPONSE"; } else if (error instanceof ExtendedAgentCardNotConfiguredError) { status = Status.FAILED_PRECONDITION; - description = "ExtendedCardNotConfiguredError: " + error.getMessage(); + description = "ExtendedAgentCardNotConfiguredError: " + error.getMessage(); errorReason = "EXTENDED_AGENT_CARD_NOT_CONFIGURED"; } else if (error instanceof ExtensionSupportRequiredError) { status = Status.FAILED_PRECONDITION; @@ -800,10 +804,7 @@ private void handleError(StreamObserver responseObserver, A2AError error) // Create metadata with grpc-status-details-bin io.grpc.Metadata trailers = new io.grpc.Metadata(); - io.grpc.Metadata.Key statusKey = - io.grpc.Metadata.Key.of("grpc-status-details-bin", - io.grpc.protobuf.ProtoUtils.metadataMarshaller(com.google.rpc.Status.getDefaultInstance())); - trailers.put(statusKey, rpcStatus); + trailers.put(GRPC_STATUS_DETAILS_KEY, rpcStatus); // Send error with ErrorInfo in metadata responseObserver.onError(status.withDescription(description).asRuntimeException(trailers));