Skip to content

feat(errors): add structured error codes and details to A2A error types#743

Open
ehsavoie wants to merge 1 commit intoa2aproject:mainfrom
ehsavoie:errorinfo
Open

feat(errors): add structured error codes and details to A2A error types#743
ehsavoie wants to merge 1 commit intoa2aproject:mainfrom
ehsavoie:errorinfo

Conversation

@ehsavoie
Copy link
Collaborator

Replace brittle string-matching error detection in gRPC and REST transports with a structured approach using error codes (A2AErrorCodes) and a details field. The GrpcErrorMapper now extracts ErrorInfo from gRPC status details via a REASON_MAP lookup, and error types carry richer context through a dedicated details field.
🦕

@ehsavoie ehsavoie requested review from jmesnil and kabir and removed request for kabir March 14, 2026 10:39
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the error handling mechanisms across gRPC and REST transports by introducing a structured error system. By moving away from string-based error identification to explicit error codes and a detailed context map, the changes aim to provide more consistent, machine-readable, and robust error information. This improvement will streamline debugging, facilitate better error-driven logic, and ensure greater interoperability within the A2A protocol.

Highlights

  • Structured Error Codes: Replaced brittle string-matching error detection in gRPC and REST transports with a structured approach using A2AErrorCodes and a dedicated 'details' field for richer context.
  • gRPC Error Mapping: The GrpcErrorMapper now extracts google.rpc.ErrorInfo from gRPC status details via a REASON_MAP lookup, providing more precise error identification.
  • REST Error Handling: REST error responses now conform to a structured format similar to Google Cloud API errors, including reason and domain fields within the details array, replacing the previous RFC 7807 Problem Details format.
  • A2AError and A2AErrorCodes Refactor: The base A2AError class now uses a Map<String, Object> for its details field instead of a generic Object data. A2AErrorCodes has been converted from an interface to an enum, providing explicit mappings for JSON-RPC codes, gRPC status names, and HTTP status codes.
  • JSON-RPC Serialization: JSON-RPC error serialization and deserialization have been updated to correctly handle the new details map, ensuring consistency across transports.
Changelog
  • client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/GrpcErrorMapper.java
    • Updated imports to include Map, InvalidProtocolBufferException, Nullable, A2AErrorCodes, and StatusProto.
    • Introduced a REASON_MAP for mapping string reasons to A2AErrorCodes.
    • Refactored mapGrpcError to prioritize extracting ErrorInfo from gRPC status details using StatusProto.
    • Added extractReason and mapByErrorCode helper methods for structured error processing.
    • Converted error mapping logic to use Java 17 switch expressions and removed old string-matching logic.
  • client/transport/grpc/src/test/java/io/a2a/client/transport/grpc/GrpcErrorMapperTest.java
    • Removed unused assertEquals import.
    • Added imports for Any and ErrorInfo for structured error testing.
    • Introduced createA2AStatusException helper to generate gRPC StatusRuntimeException with ErrorInfo.
    • Updated all existing test cases to utilize the new createA2AStatusException for creating gRPC exceptions.
    • Modified testFallbackToStatusCodeMapping to specifically test scenarios without ErrorInfo details.
  • client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransport.java
    • Modified unmarshalResponse to use error.getDetails() instead of error.getData() for constructing error messages.
  • client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportTest.java
    • Updated error message assertion in testA2AClientSendMessageWithError to match the new details map format.
  • client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JsonMessages.java
    • Updated JSON error response structure from data to details in test messages.
  • client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JsonStreamingMessages.java
    • Updated JSON error response structure from data to details in streaming test messages.
  • client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/sse/SSEEventListenerTest.java
    • Updated testOnEventWithError assertion to retrieve error details from the details map.
  • client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestErrorMapper.java
    • Added imports for Map and A2AErrorCodes.
    • Introduced a REASON_MAP for mapping string reasons to A2AErrorCodes.
    • Modified mapRestError to parse Google Cloud API error format and extract the reason from details.
    • Added extractReason helper method.
    • Replaced mapRestErrorByType with mapRestErrorByReason for structured error handling.
    • Updated mapRestErrorByClassName to use A2AErrorCodes for default values.
  • jsonrpc-common/src/main/java/io/a2a/jsonrpc/common/json/JsonUtil.java
    • Replaced direct A2AErrorCodes constant imports with io.a2a.spec.A2AErrorCodes.
    • Updated ThrowableTypeAdapter to define THROWABLE_MARKER_FIELD.
    • Refactored A2AErrorTypeAdapter to handle details (Map) instead of data (Object).
    • Replaced readDataValue with readDetailsValue for map parsing.
    • Modified createErrorInstance to use A2AErrorCodes.fromCode() and a switch expression for error instantiation.
    • Added imports for new A2AProtocolError subclasses.
  • jsonrpc-common/src/test/java/io/a2a/jsonrpc/common/json/A2AErrorSerializationTest.java
    • Removed direct A2AErrorCodes constant imports.
    • Updated shouldDeserializeToCorrectA2AErrorSubclass test to use A2AErrorCodes.ENUM.code() and verify details map content.
  • reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java
    • Updated Javadoc example for error response to use the details field.
  • reference/rest/src/test/java/io/a2a/server/rest/quarkus/A2AServerRoutesTest.java
    • Updated mock error responses to reflect the new Google Cloud API error format with details and reason.
    • Modified assertions to check for specific reason values instead of RFC 7807 type URIs.
  • reference/rest/src/test/java/io/a2a/server/rest/quarkus/QuarkusA2ARestTest.java
    • Updated assertions to check for CONTENT_TYPE_NOT_SUPPORTED and VERSION_NOT_SUPPORTED reasons in the response body.
  • spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java
    • Replaced direct A2AErrorCodes constant imports with io.a2a.spec.A2AErrorCodes and added Map import.
    • Modified processError to parse details as a Map<String, Object>.
    • Updated processError to use A2AErrorCodes.fromCode() and a switch expression for error instantiation.
    • Modified toJsonRPCErrorResponse to serialize the details map.
  • spec/src/main/java/io/a2a/spec/A2AError.java
    • Added Map import.
    • Changed the data field from Object to Map<String, Object> details.
    • Updated constructor to initialize details as an unmodifiable map, defaulting to an empty map.
    • Replaced getData() with getDetails().
  • spec/src/main/java/io/a2a/spec/A2AErrorCodes.java
    • Converted A2AErrorCodes from an interface with int constants to an enum.
    • Each enum constant now includes JSON-RPC code, gRPC grpcStatus, and HTTP httpCode.
    • Added code(), grpcStatus(), httpCode() methods and a static fromCode(int code) lookup method.
  • spec/src/main/java/io/a2a/spec/A2AProtocolError.java
    • Added Map import.
    • Removed the url field and its getter.
    • Updated constructor to accept Map<String, Object> details and pass it to the superclass.
    • Simplified class description.
  • spec/src/main/java/io/a2a/spec/ContentTypeNotSupportedError.java
    • Added Map import.
    • Updated constructor to accept Map<String, Object> details and use A2AErrorCodes.CONTENT_TYPE_NOT_SUPPORTED.code().
  • spec/src/main/java/io/a2a/spec/ExtendedAgentCardNotConfiguredError.java
    • Added Map import.
    • Updated constructor to accept Map<String, Object> details and use A2AErrorCodes.EXTENDED_AGENT_CARD_NOT_CONFIGURED.code().
  • spec/src/main/java/io/a2a/spec/ExtensionSupportRequiredError.java
    • Added Map import.
    • Updated constructor to accept Map<String, Object> details and use A2AErrorCodes.EXTENSION_SUPPORT_REQUIRED.code().
  • spec/src/main/java/io/a2a/spec/InternalError.java
    • Added Map import.
    • Updated constructor to accept Map<String, Object> details and use A2AErrorCodes.INTERNAL.code().
  • spec/src/main/java/io/a2a/spec/InvalidAgentResponseError.java
    • Added Map import.
    • Updated constructor to accept Map<String, Object> details and use A2AErrorCodes.INVALID_AGENT_RESPONSE.code().
  • spec/src/main/java/io/a2a/spec/InvalidParamsError.java
    • Added Map import.
    • Updated constructor to accept Map<String, Object> details and use A2AErrorCodes.INVALID_PARAMS.code().
  • spec/src/main/java/io/a2a/spec/InvalidRequestError.java
    • Added Map import.
    • Updated constructor to accept Map<String, Object> details and use A2AErrorCodes.INVALID_REQUEST.code().
  • spec/src/main/java/io/a2a/spec/JSONParseError.java
    • Added Map import.
    • Updated constructor to accept Map<String, Object> details and use A2AErrorCodes.JSON_PARSE.code().
  • spec/src/main/java/io/a2a/spec/MethodNotFoundError.java
    • Added Map import.
    • Updated constructors to accept Map<String, Object> details and use A2AErrorCodes.METHOD_NOT_FOUND.code().
  • spec/src/main/java/io/a2a/spec/PushNotificationNotSupportedError.java
    • Added Map import.
    • Updated constructor to accept Map<String, Object> details and use A2AErrorCodes.PUSH_NOTIFICATION_NOT_SUPPORTED.code().
  • spec/src/main/java/io/a2a/spec/TaskNotCancelableError.java
    • Added Map import.
    • Updated constructor to accept Map<String, Object> details and use A2AErrorCodes.TASK_NOT_CANCELABLE.code().
  • spec/src/main/java/io/a2a/spec/TaskNotFoundError.java
    • Added Map import.
    • Updated constructor to accept Map<String, Object> details and use A2AErrorCodes.TASK_NOT_FOUND.code().
  • spec/src/main/java/io/a2a/spec/UnsupportedOperationError.java
    • Added Map import.
    • Updated constructor to accept Map<String, Object> details and use A2AErrorCodes.UNSUPPORTED_OPERATION.code().
  • spec/src/main/java/io/a2a/spec/VersionNotSupportedError.java
    • Added Map import.
    • Updated constructor to accept Map<String, Object> details and use A2AErrorCodes.VERSION_NOT_SUPPORTED.code().
  • transport/grpc/src/main/java/io/a2a/transport/grpc/handler/GrpcHandler.java
    • Added imports for A2AErrorCodes and StatusProto.
    • Refactored handleError to use A2AErrorCodes enum for gRPC status and reason determination.
    • Constructed com.google.rpc.ErrorInfo with reason, domain, and metadata from A2AError.getDetails().
    • Used StatusProto.toStatusRuntimeException to embed structured ErrorInfo in gRPC exceptions.
    • Removed the large if-else if block for error type mapping.
  • transport/grpc/src/test/java/io/a2a/transport/grpc/handler/GrpcHandlerTest.java
    • Modified assertGrpcError to verify the presence and content of com.google.rpc.ErrorInfo in gRPC status details.
  • transport/rest/src/main/java/io/a2a/transport/rest/handler/RestHandler.java
    • Replaced direct JSON_PARSE_ERROR_CODE import with io.a2a.spec.A2AErrorCodes.
    • Removed imports for specific error types as error mapping is now centralized.
    • Modified validate method to throw JSONParseError with null details.
    • Refactored mapErrorToHttpStatus to use A2AErrorCodes.fromCode() and errorCode.httpCode().
    • Removed mapErrorToURI method.
    • Refactored HTTPRestErrorResponse to generate Google Cloud API error format with nested ErrorBody and ErrorDetail.
  • transport/rest/src/test/java/io/a2a/transport/rest/handler/RestHandlerTest.java
    • Updated assertProblemDetail helper to verify the new Google Cloud API error format.
    • Updated all test calls to assertProblemDetail to pass the expected reason string.
    • Modified testSendMessageWrongValueBody assertion to check the new error structure.
    • Updated streaming tests to parse the new error JSON structure and check the reason field.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@ehsavoie
Copy link
Collaborator Author

@kabir @jmesnil this is a tentative fix of the last minute changes in the error format. It doesn't cover jsonrpc (I'm not sure if something was required there). If it is not sufficient feel free to close it and start again of from it.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a significant and valuable refactoring of the error handling mechanism across gRPC and REST transports. The move from brittle string-matching on error descriptions to a structured approach using A2AErrorCodes and a details map is a major improvement for robustness and maintainability. The introduction of the A2AErrorCodes enum and the adoption of standard error formats (Google Cloud API for REST and ErrorInfo for gRPC) are excellent changes.

The refactoring has been applied consistently across the codebase, including the specification classes, transport handlers, and utility classes. The tests have also been thoroughly updated to reflect and validate the new structured error handling.

I've found a potential bug in the fallback error mapping in GrpcErrorMapper that could lead to a NullPointerException, and a couple of minor improvements in JSONRPCUtils regarding Gson instance reuse, which also align with project rules on consistency. Please see the detailed comments.

Overall, this is a high-quality contribution that greatly improves the project's error handling infrastructure.

Replace brittle string-matching error detection in gRPC and REST transports
with a structured approach using error codes (A2AErrorCodes) and a details
field. The GrpcErrorMapper now extracts ErrorInfo from gRPC status details
via a REASON_MAP lookup, and error types carry richer context through a
dedicated details field.

Signed-off-by: Emmanuel Hugonnet <ehugonne@redhat.com>
@ehsavoie
Copy link
Collaborator Author

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request is a significant and valuable refactoring of the error handling mechanism, moving from brittle string-matching to a structured, code-based approach. The introduction of A2AErrorCodes, the use of google.rpc.ErrorInfo in gRPC, and the adoption of a consistent Google Cloud API-style error format for REST are all excellent improvements that will enhance the robustness and maintainability of the error handling logic. My review focuses on a few areas where the new implementation could be made even more robust by ensuring the new structured error details are fully preserved across the transport layer and that error codes are mapped with appropriate specificity.

Comment on lines +82 to +99
private static @Nullable String extractReason(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.getReason();
}
}
}
}
} catch (InvalidProtocolBufferException ignored) {
// Fall through to status code-based mapping
}
return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The extractReason method currently only extracts the reason string from the ErrorInfo details and discards the associated metadata map. This leads to a loss of the structured error details that are sent by the server.

To preserve this information, I recommend modifying this method to return both the reason and the metadata. You could use a simple record or a Pair for this. The call chain (mapGrpcError and mapByErrorCode) should then be updated to propagate this metadata into the details field of the created A2AError instances, ensuring no data is lost during error mapping.

Comment on lines +87 to +98
private static @org.jspecify.annotations.Nullable String extractReason(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")) {
return detail.get("reason").getAsString();
}
}
}
return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

high

Similar to the issue in GrpcErrorMapper, this extractReason method only extracts the reason string from the error details object and ignores the metadata field within the same object. This results in the loss of structured error details sent by the server.

To fix this, this method should be updated to extract and return both the reason and the metadata map. Subsequently, mapRestErrorByReason should be modified to use this metadata when constructing the A2AError instances, ensuring the details are preserved.

case EXTENDED_AGENT_CARD_NOT_CONFIGURED -> new A2AClientException(fullMessage, new ExtendedAgentCardNotConfiguredError(null, errorMessage, null));
case EXTENSION_SUPPORT_REQUIRED -> new A2AClientException(fullMessage, new ExtensionSupportRequiredError(null, errorMessage, null));
case VERSION_NOT_SUPPORTED -> new A2AClientException(fullMessage, new VersionNotSupportedError(null, errorMessage, null));
case INVALID_REQUEST, JSON_PARSE -> new A2AClientException(fullMessage, new InvalidRequestError());
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The JSON_PARSE error code is being mapped to InvalidRequestError, which loses the specific error type. JSONParseError is a distinct class and should be instantiated here to maintain error specificity. This appears to be a regression from previous behavior.

This case should be split to handle INVALID_REQUEST and JSON_PARSE separately, instantiating InvalidRequestError and io.a2a.spec.JSONParseError respectively.

References
  1. Adhere to the project's specification for gRPC error code mappings, ensuring specific error types are instantiated to maintain error specificity.

Comment on lines +732 to 734
if (!error.getDetails().isEmpty()) {
error.getDetails().forEach((k, v) -> errorInfoBuilder.putMetadata(k, String.valueOf(v)));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Using String.valueOf(v) to convert all values from the details map to strings can lead to data loss or non-standard serialization for complex objects like nested maps or lists. The ErrorInfo metadata map only supports Map<String, String>, but to preserve the structure of complex details, it would be more robust to serialize non-string values to a standard format like JSON.

Consider checking the type of the value and serializing it to JSON if it's not a simple string. This would make the error details more reliably transferable, although it would require the client to handle potential JSON-encoded values in the metadata.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant