Skip to content

Commit 2ca7481

Browse files
committed
feat(errors): add structured error codes and details to A2A error types
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>
1 parent f81ffd1 commit 2ca7481

35 files changed

Lines changed: 649 additions & 632 deletions

File tree

Lines changed: 87 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
package io.a2a.client.transport.grpc;
22

3+
import java.util.HashMap;
4+
import java.util.Map;
5+
6+
import com.google.protobuf.InvalidProtocolBufferException;
7+
import org.jspecify.annotations.Nullable;
38
import io.a2a.common.A2AErrorMessages;
49
import io.a2a.spec.A2AClientException;
10+
import io.a2a.spec.A2AErrorCodes;
511
import io.a2a.spec.ContentTypeNotSupportedError;
612
import io.a2a.spec.ExtendedAgentCardNotConfiguredError;
713
import io.a2a.spec.ExtensionSupportRequiredError;
@@ -16,70 +22,102 @@
1622
import io.a2a.spec.UnsupportedOperationError;
1723
import io.a2a.spec.VersionNotSupportedError;
1824
import io.grpc.Status;
25+
import io.grpc.protobuf.StatusProto;
1926

2027
/**
21-
* Utility class to map gRPC exceptions to appropriate A2A error types
28+
* Utility class to map gRPC exceptions to appropriate A2A error types.
29+
* <p>
30+
* Extracts {@code google.rpc.ErrorInfo} from gRPC status details to identify the
31+
* specific A2A error type via the {@code reason} field.
2232
*/
2333
public class GrpcErrorMapper {
2434

35+
private static final Map<String, A2AErrorCodes> REASON_MAP = Map.ofEntries(
36+
Map.entry("TASK_NOT_FOUND", A2AErrorCodes.TASK_NOT_FOUND),
37+
Map.entry("TASK_NOT_CANCELABLE", A2AErrorCodes.TASK_NOT_CANCELABLE),
38+
Map.entry("PUSH_NOTIFICATION_NOT_SUPPORTED", A2AErrorCodes.PUSH_NOTIFICATION_NOT_SUPPORTED),
39+
Map.entry("UNSUPPORTED_OPERATION", A2AErrorCodes.UNSUPPORTED_OPERATION),
40+
Map.entry("CONTENT_TYPE_NOT_SUPPORTED", A2AErrorCodes.CONTENT_TYPE_NOT_SUPPORTED),
41+
Map.entry("INVALID_AGENT_RESPONSE", A2AErrorCodes.INVALID_AGENT_RESPONSE),
42+
Map.entry("EXTENDED_AGENT_CARD_NOT_CONFIGURED", A2AErrorCodes.EXTENDED_AGENT_CARD_NOT_CONFIGURED),
43+
Map.entry("EXTENSION_SUPPORT_REQUIRED", A2AErrorCodes.EXTENSION_SUPPORT_REQUIRED),
44+
Map.entry("VERSION_NOT_SUPPORTED", A2AErrorCodes.VERSION_NOT_SUPPORTED),
45+
Map.entry("INVALID_REQUEST", A2AErrorCodes.INVALID_REQUEST),
46+
Map.entry("METHOD_NOT_FOUND", A2AErrorCodes.METHOD_NOT_FOUND),
47+
Map.entry("INVALID_PARAMS", A2AErrorCodes.INVALID_PARAMS),
48+
Map.entry("INTERNAL", A2AErrorCodes.INTERNAL),
49+
Map.entry("JSON_PARSE", A2AErrorCodes.JSON_PARSE)
50+
);
51+
2552
public static A2AClientException mapGrpcError(Throwable e) {
2653
return mapGrpcError(e, "gRPC error: ");
2754
}
2855

2956
public static A2AClientException mapGrpcError(Throwable e, String errorPrefix) {
3057
Status status = Status.fromThrowable(e);
3158
Status.Code code = status.getCode();
32-
String description = status.getDescription();
33-
34-
// Extract the actual error type from the description if possible
35-
// (using description because the same code can map to multiple errors -
36-
// see GrpcHandler#handleError)
37-
if (description != null) {
38-
if (description.contains("TaskNotFoundError")) {
39-
return new A2AClientException(errorPrefix + description, new TaskNotFoundError());
40-
} else if (description.contains("UnsupportedOperationError")) {
41-
return new A2AClientException(errorPrefix + description, new UnsupportedOperationError());
42-
} else if (description.contains("InvalidParamsError")) {
43-
return new A2AClientException(errorPrefix + description, new InvalidParamsError());
44-
} else if (description.contains("InvalidRequestError")) {
45-
return new A2AClientException(errorPrefix + description, new InvalidRequestError());
46-
} else if (description.contains("MethodNotFoundError")) {
47-
return new A2AClientException(errorPrefix + description, new MethodNotFoundError());
48-
} else if (description.contains("TaskNotCancelableError")) {
49-
return new A2AClientException(errorPrefix + description, new TaskNotCancelableError());
50-
} else if (description.contains("PushNotificationNotSupportedError")) {
51-
return new A2AClientException(errorPrefix + description, new PushNotificationNotSupportedError());
52-
} else if (description.contains("JSONParseError")) {
53-
return new A2AClientException(errorPrefix + description, new JSONParseError());
54-
} else if (description.contains("ContentTypeNotSupportedError")) {
55-
return new A2AClientException(errorPrefix + description, new ContentTypeNotSupportedError(null, description, null));
56-
} else if (description.contains("InvalidAgentResponseError")) {
57-
return new A2AClientException(errorPrefix + description, new InvalidAgentResponseError(null, description, null));
58-
} else if (description.contains("ExtendedCardNotConfiguredError")) {
59-
return new A2AClientException(errorPrefix + description, new ExtendedAgentCardNotConfiguredError(null, description, null));
60-
} else if (description.contains("ExtensionSupportRequiredError")) {
61-
return new A2AClientException(errorPrefix + description, new ExtensionSupportRequiredError(null, description, null));
62-
} else if (description.contains("VersionNotSupportedError")) {
63-
return new A2AClientException(errorPrefix + description, new VersionNotSupportedError(null, description, null));
59+
String message = status.getDescription();
60+
61+
// Try to extract ErrorInfo from status details
62+
com.google.rpc.@Nullable ErrorInfo errorInfo = extractErrorInfo(e);
63+
if (errorInfo != null) {
64+
A2AErrorCodes errorCode = REASON_MAP.get(errorInfo.getReason());
65+
if (errorCode != null) {
66+
String errorMessage = message != null ? message : (e.getMessage() != null ? e.getMessage() : "");
67+
Map<String, Object> metadata = errorInfo.getMetadataMap().isEmpty() ? null
68+
: new HashMap<String, Object>(errorInfo.getMetadataMap());
69+
return mapByErrorCode(errorCode, errorPrefix + errorMessage, errorMessage, metadata);
6470
}
6571
}
66-
72+
6773
// Fall back to mapping based on status code
68-
switch (code) {
69-
case NOT_FOUND:
70-
return new A2AClientException(errorPrefix + (description != null ? description : e.getMessage()), new TaskNotFoundError());
71-
case UNIMPLEMENTED:
72-
return new A2AClientException(errorPrefix + (description != null ? description : e.getMessage()), new UnsupportedOperationError());
73-
case INVALID_ARGUMENT:
74-
return new A2AClientException(errorPrefix + (description != null ? description : e.getMessage()), new InvalidParamsError());
75-
case INTERNAL:
76-
return new A2AClientException(errorPrefix + (description != null ? description : e.getMessage()), new io.a2a.spec.InternalError(null, e.getMessage(), null));
77-
case UNAUTHENTICATED:
78-
return new A2AClientException(errorPrefix + A2AErrorMessages.AUTHENTICATION_FAILED);
79-
case PERMISSION_DENIED:
80-
return new A2AClientException(errorPrefix + A2AErrorMessages.AUTHORIZATION_FAILED);
81-
default:
82-
return new A2AClientException(errorPrefix + e.getMessage(), e);
74+
String desc = message != null ? message : e.getMessage() == null ? "" : e.getMessage();
75+
return switch (code) {
76+
case NOT_FOUND -> new A2AClientException(errorPrefix + desc, new TaskNotFoundError());
77+
case UNIMPLEMENTED -> new A2AClientException(errorPrefix + desc, new UnsupportedOperationError());
78+
case INVALID_ARGUMENT -> new A2AClientException(errorPrefix + desc, new InvalidParamsError());
79+
case INTERNAL -> new A2AClientException(errorPrefix + desc, new io.a2a.spec.InternalError(null, desc, null));
80+
case UNAUTHENTICATED -> new A2AClientException(errorPrefix + A2AErrorMessages.AUTHENTICATION_FAILED);
81+
case PERMISSION_DENIED -> new A2AClientException(errorPrefix + A2AErrorMessages.AUTHORIZATION_FAILED);
82+
default -> new A2AClientException(errorPrefix + e.getMessage(), e);
83+
};
84+
}
85+
86+
private static com.google.rpc.@Nullable ErrorInfo extractErrorInfo(Throwable e) {
87+
try {
88+
com.google.rpc.Status rpcStatus = StatusProto.fromThrowable(e);
89+
if (rpcStatus != null) {
90+
for (com.google.protobuf.Any detail : rpcStatus.getDetailsList()) {
91+
if (detail.is(com.google.rpc.ErrorInfo.class)) {
92+
com.google.rpc.ErrorInfo errorInfo = detail.unpack(com.google.rpc.ErrorInfo.class);
93+
if ("a2a-protocol.org".equals(errorInfo.getDomain())) {
94+
return errorInfo;
95+
}
96+
}
97+
}
98+
}
99+
} catch (InvalidProtocolBufferException ignored) {
100+
// Fall through to status code-based mapping
83101
}
102+
return null;
103+
}
104+
105+
private static A2AClientException mapByErrorCode(A2AErrorCodes errorCode, String fullMessage, String errorMessage, @Nullable Map<String, Object> metadata) {
106+
return switch (errorCode) {
107+
case TASK_NOT_FOUND -> new A2AClientException(fullMessage, new TaskNotFoundError(errorMessage, metadata));
108+
case TASK_NOT_CANCELABLE -> new A2AClientException(fullMessage, new TaskNotCancelableError(null, errorMessage, metadata));
109+
case PUSH_NOTIFICATION_NOT_SUPPORTED -> new A2AClientException(fullMessage, new PushNotificationNotSupportedError(null, errorMessage, metadata));
110+
case UNSUPPORTED_OPERATION -> new A2AClientException(fullMessage, new UnsupportedOperationError(null, errorMessage, metadata));
111+
case CONTENT_TYPE_NOT_SUPPORTED -> new A2AClientException(fullMessage, new ContentTypeNotSupportedError(null, errorMessage, metadata));
112+
case INVALID_AGENT_RESPONSE -> new A2AClientException(fullMessage, new InvalidAgentResponseError(null, errorMessage, metadata));
113+
case EXTENDED_AGENT_CARD_NOT_CONFIGURED -> new A2AClientException(fullMessage, new ExtendedAgentCardNotConfiguredError(null, errorMessage, metadata));
114+
case EXTENSION_SUPPORT_REQUIRED -> new A2AClientException(fullMessage, new ExtensionSupportRequiredError(null, errorMessage, metadata));
115+
case VERSION_NOT_SUPPORTED -> new A2AClientException(fullMessage, new VersionNotSupportedError(null, errorMessage, metadata));
116+
case INVALID_REQUEST -> new A2AClientException(fullMessage, new InvalidRequestError(null, errorMessage, metadata));
117+
case JSON_PARSE -> new A2AClientException(fullMessage, new JSONParseError(null, errorMessage, metadata));
118+
case METHOD_NOT_FOUND -> new A2AClientException(fullMessage, new MethodNotFoundError(null, errorMessage, metadata));
119+
case INVALID_PARAMS -> new A2AClientException(fullMessage, new InvalidParamsError(null, errorMessage, metadata));
120+
case INTERNAL -> new A2AClientException(fullMessage, new io.a2a.spec.InternalError(null, errorMessage, metadata));
121+
};
84122
}
85123
}

0 commit comments

Comments
 (0)