The a2a-grpc client maps incoming gRPC errors to A2A errors using only the
tonic::Status code. It never decodes the google.rpc.Status / ErrorInfo carried
in the grpc-status-details-bin trailer. Because A2A spec §5.4 maps three distinct
errors to the same gRPC status FAILED_PRECONDITION, the client cannot tell them
apart and unconditionally coerces all of them to -32002 (TaskNotCancelable):
| A2A error |
JSON-RPC |
gRPC status |
| TaskNotCancelableError |
-32002 |
FAILED_PRECONDITION |
| PushNotificationNotSupportedError |
-32003 |
FAILED_PRECONDITION |
| UnsupportedOperationError |
-32004 |
FAILED_PRECONDITION |
The specific error is meant to be recovered from the ErrorInfo detail
(reason = UPPER_SNAKE_CASE name, domain = a2a-protocol.org, AIP-193 rich error
model). The Go, Python, and .NET clients all read it; the Rust gRPC client does not.
Location
a2a-grpc/src/errors.rs, status_to_a2a_error (lines 29–40):
pub fn status_to_a2a_error(status: &tonic::Status) -> A2AError {
let code = match status.code() { // only the gRPC code is inspected
tonic::Code::NotFound => error_code::TASK_NOT_FOUND,
tonic::Code::FailedPrecondition => error_code::TASK_NOT_CANCELABLE, // fixed -32002
...
};
A2AError::new(code, status.message()) // status.details() never read
}
Every gRPC client method routes errors through this function
(a2a-grpc/src/client.rs, .map_err(|s| status_to_a2a_error(&s))).
status.details() is never called anywhere in the crate.
Steps to reproduce
Call CreateTaskPushNotificationConfig (gRPC) against a server that does not support
push notifications. The server returns FAILED_PRECONDITION with a correct
ErrorInfo (reason=PUSH_NOTIFICATION_NOT_SUPPORTED, domain=a2a-protocol.org).
Expected: A2A code -32003 (PushNotificationNotSupported)
Actual: A2A code -32002 (TaskNotCancelable)
Observed in the agntcy-a2a interop matrix (Rust client → .NET server, gRPC). Go and
Python clients recover -32003 correctly from the same response.
Note
The SDK already has the logic to fix this — a2a-client/src/lib.rs
a2a_error_from_details() reads ErrorInfo, checks domain == "a2a-protocol.org",
and maps reason via reason_to_error_code(). It is wired into the JSON-RPC and REST
clients (jsonrpc.rs, rest.rs) but not the gRPC client. The gRPC path should
decode status.details() as google.rpc.Status, extract the ErrorInfo, and reuse
reason_to_error_code() — mirroring the other transports.
The
a2a-grpcclient maps incoming gRPC errors to A2A errors using only thetonic::Statuscode. It never decodes thegoogle.rpc.Status/ErrorInfocarriedin the
grpc-status-details-bintrailer. Because A2A spec §5.4 maps three distincterrors to the same gRPC status
FAILED_PRECONDITION, the client cannot tell themapart and unconditionally coerces all of them to
-32002(TaskNotCancelable):The specific error is meant to be recovered from the
ErrorInfodetail(
reason= UPPER_SNAKE_CASE name,domain=a2a-protocol.org, AIP-193 rich errormodel). The Go, Python, and .NET clients all read it; the Rust gRPC client does not.
Location
a2a-grpc/src/errors.rs,status_to_a2a_error(lines 29–40):Every gRPC client method routes errors through this function
(a2a-grpc/src/client.rs, .map_err(|s| status_to_a2a_error(&s))).
status.details() is never called anywhere in the crate.
Steps to reproduce
Call CreateTaskPushNotificationConfig (gRPC) against a server that does not support
push notifications. The server returns FAILED_PRECONDITION with a correct
ErrorInfo (reason=PUSH_NOTIFICATION_NOT_SUPPORTED, domain=a2a-protocol.org).
Expected: A2A code -32003 (PushNotificationNotSupported)
Actual: A2A code -32002 (TaskNotCancelable)
Observed in the agntcy-a2a interop matrix (Rust client → .NET server, gRPC). Go and
Python clients recover -32003 correctly from the same response.
Note
The SDK already has the logic to fix this — a2a-client/src/lib.rs
a2a_error_from_details() reads ErrorInfo, checks domain == "a2a-protocol.org",
and maps reason via reason_to_error_code(). It is wired into the JSON-RPC and REST
clients (jsonrpc.rs, rest.rs) but not the gRPC client. The gRPC path should
decode status.details() as google.rpc.Status, extract the ErrorInfo, and reuse
reason_to_error_code() — mirroring the other transports.