Skip to content

Commit ed290be

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 26cb3ad commit ed290be

File tree

35 files changed

+627
-665
lines changed

35 files changed

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

3+
import java.util.Map;
4+
5+
import com.google.protobuf.InvalidProtocolBufferException;
6+
import org.jspecify.annotations.Nullable;
37
import io.a2a.common.A2AErrorMessages;
48
import io.a2a.spec.A2AClientException;
9+
import io.a2a.spec.A2AErrorCodes;
510
import io.a2a.spec.ContentTypeNotSupportedError;
611
import io.a2a.spec.ExtendedAgentCardNotConfiguredError;
712
import io.a2a.spec.ExtensionSupportRequiredError;
@@ -16,70 +21,99 @@
1621
import io.a2a.spec.UnsupportedOperationError;
1722
import io.a2a.spec.VersionNotSupportedError;
1823
import io.grpc.Status;
24+
import io.grpc.protobuf.StatusProto;
1925

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

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

2955
public static A2AClientException mapGrpcError(Throwable e, String errorPrefix) {
3056
Status status = Status.fromThrowable(e);
3157
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));
58+
String message = status.getDescription();
59+
60+
// Try to extract ErrorInfo from status details
61+
String reason = extractReason(e);
62+
if (reason != null) {
63+
A2AErrorCodes errorCode = REASON_MAP.get(reason);
64+
if (errorCode != null) {
65+
String errorMessage = message != null ? message : (e.getMessage() != null ? e.getMessage() : "");
66+
return mapByErrorCode(errorCode, errorPrefix + errorMessage, errorMessage);
6467
}
6568
}
66-
69+
6770
// 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);
71+
String desc = message != null ? message : e.getMessage();
72+
return switch (code) {
73+
case NOT_FOUND -> new A2AClientException(errorPrefix + desc, new TaskNotFoundError());
74+
case UNIMPLEMENTED -> new A2AClientException(errorPrefix + desc, new UnsupportedOperationError());
75+
case INVALID_ARGUMENT -> new A2AClientException(errorPrefix + desc, new InvalidParamsError());
76+
case INTERNAL -> new A2AClientException(errorPrefix + desc, new io.a2a.spec.InternalError(null, desc, null));
77+
case UNAUTHENTICATED -> new A2AClientException(errorPrefix + A2AErrorMessages.AUTHENTICATION_FAILED);
78+
case PERMISSION_DENIED -> new A2AClientException(errorPrefix + A2AErrorMessages.AUTHORIZATION_FAILED);
79+
default -> new A2AClientException(errorPrefix + e.getMessage(), e);
80+
};
81+
}
82+
83+
private static @Nullable String extractReason(Throwable e) {
84+
try {
85+
com.google.rpc.Status rpcStatus = StatusProto.fromThrowable(e);
86+
if (rpcStatus != null) {
87+
for (com.google.protobuf.Any detail : rpcStatus.getDetailsList()) {
88+
if (detail.is(com.google.rpc.ErrorInfo.class)) {
89+
com.google.rpc.ErrorInfo errorInfo = detail.unpack(com.google.rpc.ErrorInfo.class);
90+
if ("a2a-protocol.org".equals(errorInfo.getDomain())) {
91+
return errorInfo.getReason();
92+
}
93+
}
94+
}
95+
}
96+
} catch (InvalidProtocolBufferException ignored) {
97+
// Fall through to status code-based mapping
8398
}
99+
return null;
100+
}
101+
102+
private static A2AClientException mapByErrorCode(A2AErrorCodes errorCode, String fullMessage, String errorMessage) {
103+
return switch (errorCode) {
104+
case TASK_NOT_FOUND -> new A2AClientException(fullMessage, new TaskNotFoundError());
105+
case TASK_NOT_CANCELABLE -> new A2AClientException(fullMessage, new TaskNotCancelableError());
106+
case PUSH_NOTIFICATION_NOT_SUPPORTED -> new A2AClientException(fullMessage, new PushNotificationNotSupportedError());
107+
case UNSUPPORTED_OPERATION -> new A2AClientException(fullMessage, new UnsupportedOperationError());
108+
case CONTENT_TYPE_NOT_SUPPORTED -> new A2AClientException(fullMessage, new ContentTypeNotSupportedError(null, errorMessage, null));
109+
case INVALID_AGENT_RESPONSE -> new A2AClientException(fullMessage, new InvalidAgentResponseError(null, errorMessage, null));
110+
case EXTENDED_AGENT_CARD_NOT_CONFIGURED -> new A2AClientException(fullMessage, new ExtendedAgentCardNotConfiguredError(null, errorMessage, null));
111+
case EXTENSION_SUPPORT_REQUIRED -> new A2AClientException(fullMessage, new ExtensionSupportRequiredError(null, errorMessage, null));
112+
case VERSION_NOT_SUPPORTED -> new A2AClientException(fullMessage, new VersionNotSupportedError(null, errorMessage, null));
113+
case INVALID_REQUEST, JSON_PARSE -> new A2AClientException(fullMessage, new InvalidRequestError());
114+
case METHOD_NOT_FOUND -> new A2AClientException(fullMessage, new MethodNotFoundError());
115+
case INVALID_PARAMS -> new A2AClientException(fullMessage, new InvalidParamsError());
116+
case INTERNAL -> new A2AClientException(fullMessage, new io.a2a.spec.InternalError(null, errorMessage, null));
117+
};
84118
}
85119
}

0 commit comments

Comments
 (0)