|
1 | 1 | package io.a2a.client.transport.grpc; |
2 | 2 |
|
| 3 | +import java.util.Map; |
| 4 | + |
| 5 | +import com.google.protobuf.InvalidProtocolBufferException; |
| 6 | +import org.jspecify.annotations.Nullable; |
3 | 7 | import io.a2a.common.A2AErrorMessages; |
4 | 8 | import io.a2a.spec.A2AClientException; |
| 9 | +import io.a2a.spec.A2AErrorCodes; |
5 | 10 | import io.a2a.spec.ContentTypeNotSupportedError; |
6 | 11 | import io.a2a.spec.ExtendedAgentCardNotConfiguredError; |
7 | 12 | import io.a2a.spec.ExtensionSupportRequiredError; |
|
16 | 21 | import io.a2a.spec.UnsupportedOperationError; |
17 | 22 | import io.a2a.spec.VersionNotSupportedError; |
18 | 23 | import io.grpc.Status; |
| 24 | +import io.grpc.protobuf.StatusProto; |
19 | 25 |
|
20 | 26 | /** |
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. |
22 | 31 | */ |
23 | 32 | public class GrpcErrorMapper { |
24 | 33 |
|
| 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 | + |
25 | 51 | public static A2AClientException mapGrpcError(Throwable e) { |
26 | 52 | return mapGrpcError(e, "gRPC error: "); |
27 | 53 | } |
28 | 54 |
|
29 | 55 | public static A2AClientException mapGrpcError(Throwable e, String errorPrefix) { |
30 | 56 | Status status = Status.fromThrowable(e); |
31 | 57 | 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); |
64 | 67 | } |
65 | 68 | } |
66 | | - |
| 69 | + |
67 | 70 | // 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 |
83 | 98 | } |
| 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 | + }; |
84 | 118 | } |
85 | 119 | } |
0 commit comments