Skip to main content

zeph_a2a/
error.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Error types for A2A client, server, and discovery operations.
5
6use crate::jsonrpc::JsonRpcError;
7
8/// All errors that can occur in A2A client and server operations.
9///
10/// The variants map to distinct failure modes so callers can recover appropriately:
11/// - Retry on [`Http`](A2aError::Http) (transient network issues).
12/// - Inspect the code on [`JsonRpc`](A2aError::JsonRpc) — `-32001` is task-not-found,
13///   `-32002` is not-cancelable.
14/// - Abort on [`Security`](A2aError::Security) — endpoint rejected by TLS or SSRF policy.
15#[derive(Debug, thiserror::Error)]
16pub enum A2aError {
17    /// A `reqwest` HTTP transport error (connection refused, timeout, TLS, etc.).
18    #[error("HTTP request failed: {0}")]
19    Http(#[from] reqwest::Error),
20
21    /// JSON serialization or deserialization failure.
22    #[error("JSON serialization/deserialization failed: {0}")]
23    Json(#[from] serde_json::Error),
24
25    /// The remote agent returned a JSON-RPC error object.
26    ///
27    /// Well-known codes defined by the A2A spec:
28    /// - `-32001`: task not found
29    /// - `-32002`: task not in a cancelable state
30    #[error("JSON-RPC error {code}: {message}")]
31    JsonRpc { code: i32, message: String },
32
33    /// `AgentRegistry` could not retrieve a valid [`AgentCard`](crate::types::AgentCard)
34    /// from the remote agent's well-known URL.
35    #[error("agent discovery failed for {url}: {reason}")]
36    Discovery { url: String, reason: String },
37
38    /// An error occurred while reading the SSE event stream from a streaming call.
39    #[error("SSE stream error: {0}")]
40    Stream(String),
41
42    /// An internal server-side error (binding failure, task processing panic, etc.).
43    #[error("server error: {0}")]
44    Server(String),
45
46    /// A request was rejected by the client's security policy.
47    ///
48    /// Triggered when [`A2aClient`](crate::A2aClient) is configured with
49    /// `require_tls = true` and an `http://` endpoint is used, or when
50    /// `ssrf_protection = true` and DNS resolves to a private/loopback address.
51    #[error("security policy violation: {0}")]
52    Security(String),
53}
54
55impl From<JsonRpcError> for A2aError {
56    fn from(e: JsonRpcError) -> Self {
57        Self::JsonRpc {
58            code: e.code,
59            message: e.message,
60        }
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn from_jsonrpc_error() {
70        let rpc_err = JsonRpcError {
71            code: -32001,
72            message: "task not found".into(),
73            data: None,
74        };
75        let err: A2aError = rpc_err.into();
76        match err {
77            A2aError::JsonRpc { code, message } => {
78                assert_eq!(code, -32001);
79                assert_eq!(message, "task not found");
80            }
81            _ => panic!("expected JsonRpc variant"),
82        }
83    }
84
85    #[test]
86    fn error_display() {
87        let err = A2aError::Discovery {
88            url: "http://example.com".into(),
89            reason: "connection refused".into(),
90        };
91        assert_eq!(
92            err.to_string(),
93            "agent discovery failed for http://example.com: connection refused"
94        );
95
96        let err = A2aError::Stream("unexpected EOF".into());
97        assert_eq!(err.to_string(), "SSE stream error: unexpected EOF");
98    }
99
100    #[test]
101    fn security_error_display() {
102        let err = A2aError::Security("TLS required but endpoint uses HTTP".into());
103        assert_eq!(
104            err.to_string(),
105            "security policy violation: TLS required but endpoint uses HTTP"
106        );
107    }
108
109    #[test]
110    fn from_serde_json_error() {
111        let json_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
112        let err: A2aError = json_err.into();
113        assert!(matches!(err, A2aError::Json(_)));
114    }
115}