use std::fmt;
use a2a_protocol_types::{A2aError, TaskId};
#[derive(Debug)]
#[non_exhaustive]
pub enum ClientError {
Http(hyper::Error),
HttpClient(String),
Serialization(serde_json::Error),
Protocol(A2aError),
Transport(String),
InvalidEndpoint(String),
UnexpectedStatus {
status: u16,
body: String,
retry_after: Option<std::time::Duration>,
},
AuthRequired {
task_id: TaskId,
},
Timeout(String),
TooManyPendingRequests {
limit: usize,
},
IncompleteStream {
last_event_id: Option<String>,
detail: String,
},
ProtocolBindingMismatch(String),
}
impl fmt::Display for ClientError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Http(e) => write!(f, "HTTP error: {e}"),
Self::HttpClient(msg) => write!(f, "HTTP client error: {msg}"),
Self::Serialization(e) => write!(f, "serialization error: {e}"),
Self::Protocol(e) => write!(f, "protocol error: {e}"),
Self::Transport(msg) => write!(f, "transport error: {msg}"),
Self::InvalidEndpoint(msg) => write!(f, "invalid endpoint: {msg}"),
Self::UnexpectedStatus { status, body, .. } => {
write!(f, "unexpected HTTP status {status}: {body}")
}
Self::AuthRequired { task_id } => {
write!(f, "authentication required for task: {task_id}")
}
Self::Timeout(msg) => write!(f, "timeout: {msg}"),
Self::TooManyPendingRequests { limit } => {
write!(
f,
"too many pending requests on this connection (limit {limit})"
)
}
Self::IncompleteStream { detail, .. } => {
write!(f, "stream ended before its final event: {detail}")
}
Self::ProtocolBindingMismatch(msg) => {
write!(
f,
"protocol binding mismatch: {msg}; check the agent card's supported_interfaces"
)
}
}
}
}
impl std::error::Error for ClientError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Http(e) => Some(e),
Self::Serialization(e) => Some(e),
Self::Protocol(e) => Some(e),
_ => None,
}
}
}
impl ClientError {
#[must_use]
pub const fn retry_after(&self) -> Option<std::time::Duration> {
match self {
Self::UnexpectedStatus { retry_after, .. } => *retry_after,
_ => None,
}
}
#[must_use]
pub fn is_stream_lagged(&self) -> bool {
matches!(self, Self::Protocol(e) if e.is_stream_lagged())
}
#[must_use]
pub fn dropped_event_count(&self) -> Option<u64> {
match self {
Self::Protocol(e) => e.dropped_event_count(),
_ => None,
}
}
}
#[must_use]
pub(crate) fn parse_retry_after(headers: &hyper::HeaderMap) -> Option<std::time::Duration> {
let raw = headers.get(hyper::header::RETRY_AFTER)?.to_str().ok()?;
let secs: u64 = raw.trim().parse().ok()?;
Some(std::time::Duration::from_secs(secs.min(3600)))
}
impl From<A2aError> for ClientError {
fn from(e: A2aError) -> Self {
Self::Protocol(e)
}
}
impl From<ClientError> for A2aError {
fn from(e: ClientError) -> Self {
use a2a_protocol_types::error::ErrorCode;
use a2a_protocol_types::failure::{FailureClass, set_error_class};
let class = if e.is_retryable() {
FailureClass::Transient
} else {
FailureClass::Internal
};
let code = match e {
ClientError::Protocol(inner) => return inner,
ClientError::Serialization(_) => ErrorCode::InvalidAgentResponse,
_ => ErrorCode::InternalError,
};
let mut out = Self::new(code, format!("downstream A2A call failed: {}", chain(&e)));
set_error_class(&mut out, class);
out
}
}
fn chain(e: &dyn std::error::Error) -> String {
let mut text = e.to_string();
let mut cause = e.source();
while let Some(c) = cause {
let s = c.to_string();
if !text.contains(&s) {
text.push_str(": ");
text.push_str(&s);
}
cause = c.source();
}
text
}
impl From<hyper::Error> for ClientError {
fn from(e: hyper::Error) -> Self {
Self::Http(e)
}
}
impl From<serde_json::Error> for ClientError {
fn from(e: serde_json::Error) -> Self {
Self::Serialization(e)
}
}
pub type ClientResult<T> = Result<T, ClientError>;
#[cfg(test)]
mod chain_tests;
#[cfg(test)]
mod tests;