Skip to main content

a2a_protocol_client/
error.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Tom F.
3
4//! Client error types.
5//!
6//! [`ClientError`] is the top-level error type for all A2A client operations.
7//! Use [`ClientResult`] as the return type alias.
8
9use std::fmt;
10
11use a2a_protocol_types::{A2aError, TaskId};
12
13// ── ClientError ───────────────────────────────────────────────────────────────
14
15/// Errors that can occur during A2A client operations.
16#[derive(Debug)]
17#[non_exhaustive]
18pub enum ClientError {
19    /// A transport-level HTTP error from hyper.
20    Http(hyper::Error),
21
22    /// An HTTP-level error from the hyper-util client (connection, redirect, etc.).
23    HttpClient(String),
24
25    /// JSON serialization or deserialization error.
26    Serialization(serde_json::Error),
27
28    /// A protocol-level A2A error returned by the server.
29    Protocol(A2aError),
30
31    /// A transport configuration or connection error.
32    Transport(String),
33
34    /// The agent endpoint URL is invalid or could not be resolved.
35    InvalidEndpoint(String),
36
37    /// The server returned an unexpected HTTP status code.
38    UnexpectedStatus {
39        /// The HTTP status code received.
40        status: u16,
41        /// The response body (truncated if large).
42        body: String,
43    },
44
45    /// The agent requires authentication for this task.
46    AuthRequired {
47        /// The ID of the task requiring authentication.
48        task_id: TaskId,
49    },
50
51    /// A request or stream connection timed out.
52    Timeout(String),
53}
54
55impl fmt::Display for ClientError {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        match self {
58            Self::Http(e) => write!(f, "HTTP error: {e}"),
59            Self::HttpClient(msg) => write!(f, "HTTP client error: {msg}"),
60            Self::Serialization(e) => write!(f, "serialization error: {e}"),
61            Self::Protocol(e) => write!(f, "protocol error: {e}"),
62            Self::Transport(msg) => write!(f, "transport error: {msg}"),
63            Self::InvalidEndpoint(msg) => write!(f, "invalid endpoint: {msg}"),
64            Self::UnexpectedStatus { status, body } => {
65                write!(f, "unexpected HTTP status {status}: {body}")
66            }
67            Self::AuthRequired { task_id } => {
68                write!(f, "authentication required for task: {task_id}")
69            }
70            Self::Timeout(msg) => write!(f, "timeout: {msg}"),
71        }
72    }
73}
74
75impl std::error::Error for ClientError {
76    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
77        match self {
78            Self::Http(e) => Some(e),
79            Self::Serialization(e) => Some(e),
80            Self::Protocol(e) => Some(e),
81            _ => None,
82        }
83    }
84}
85
86impl From<A2aError> for ClientError {
87    fn from(e: A2aError) -> Self {
88        Self::Protocol(e)
89    }
90}
91
92impl From<hyper::Error> for ClientError {
93    fn from(e: hyper::Error) -> Self {
94        Self::Http(e)
95    }
96}
97
98impl From<serde_json::Error> for ClientError {
99    fn from(e: serde_json::Error) -> Self {
100        Self::Serialization(e)
101    }
102}
103
104// ── ClientResult ──────────────────────────────────────────────────────────────
105
106/// Convenience type alias: `Result<T, ClientError>`.
107pub type ClientResult<T> = Result<T, ClientError>;
108
109// ── Tests ─────────────────────────────────────────────────────────────────────
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use a2a_protocol_types::ErrorCode;
115
116    #[test]
117    fn client_error_display_http_client() {
118        let e = ClientError::HttpClient("connection refused".into());
119        assert!(e.to_string().contains("connection refused"));
120    }
121
122    #[test]
123    fn client_error_display_protocol() {
124        let a2a = A2aError::task_not_found("task-99");
125        let e = ClientError::Protocol(a2a);
126        assert!(e.to_string().contains("task-99"));
127    }
128
129    #[test]
130    fn client_error_from_a2a_error() {
131        let a2a = A2aError::new(ErrorCode::TaskNotFound, "missing");
132        let e: ClientError = a2a.into();
133        assert!(matches!(e, ClientError::Protocol(_)));
134    }
135
136    #[test]
137    fn client_error_unexpected_status() {
138        let e = ClientError::UnexpectedStatus {
139            status: 404,
140            body: "Not Found".into(),
141        };
142        assert!(e.to_string().contains("404"));
143    }
144}