Skip to main content

turul_a2a_client/
error.rs

1//! Client error types.
2
3#[derive(Debug, thiserror::Error)]
4#[non_exhaustive]
5pub enum A2aClientError {
6    /// HTTP-level error with status and body.
7    #[error("HTTP {status}: {message}")]
8    Http { status: u16, message: String },
9
10    /// A2A-specific error with ErrorInfo reason.
11    #[error("A2A error {status}: {message}")]
12    A2aError {
13        status: u16,
14        message: String,
15        reason: Option<String>,
16    },
17
18    /// Request/transport error.
19    #[error("Request error: {0}")]
20    Request(#[from] reqwest::Error),
21
22    /// JSON parsing error.
23    #[error("JSON error: {0}")]
24    Json(#[from] serde_json::Error),
25
26    /// Proto-to-wrapper type conversion error.
27    #[error("Type conversion error: {0}")]
28    Conversion(String),
29
30    /// SSE stream error.
31    #[error("SSE error: {0}")]
32    Sse(String),
33
34    /// SSE stream closed unexpectedly.
35    #[error("SSE stream closed")]
36    StreamClosed,
37
38    /// gRPC transport error surfaced as a `tonic::Status`. The error
39    /// carries the full status — call `reason()` to retrieve the
40    /// `ErrorInfo.reason` set by the server (or
41    /// `None` if this is a non-A2A error).
42    #[cfg(feature = "grpc")]
43    #[error("gRPC error {0:?}: {1}")]
44    Grpc(tonic::Code, String, Option<String>),
45
46    /// gRPC transport failure before a response arrived (connection
47    /// refused, TLS failure, etc.).
48    #[cfg(feature = "grpc")]
49    #[error("gRPC transport error: {0}")]
50    GrpcTransport(String),
51}
52
53impl A2aClientError {
54    /// Get the ErrorInfo reason if this is an A2A error.
55    pub fn reason(&self) -> Option<&str> {
56        match self {
57            Self::A2aError { reason, .. } => reason.as_deref(),
58            #[cfg(feature = "grpc")]
59            Self::Grpc(_, _, reason) => reason.as_deref(),
60            _ => None,
61        }
62    }
63
64    /// Get the HTTP status code if available.
65    pub fn status(&self) -> Option<u16> {
66        match self {
67            Self::Http { status, .. } | Self::A2aError { status, .. } => Some(*status),
68            _ => None,
69        }
70    }
71
72    /// Return the gRPC status code when this is a `Grpc` variant.
73    #[cfg(feature = "grpc")]
74    pub fn grpc_code(&self) -> Option<tonic::Code> {
75        match self {
76            Self::Grpc(code, _, _) => Some(*code),
77            _ => None,
78        }
79    }
80}
81
82#[cfg(feature = "grpc")]
83impl From<tonic::Status> for A2aClientError {
84    fn from(status: tonic::Status) -> Self {
85        // Pull the ErrorInfo reason the server attached
86        // Uses tonic_types::StatusExt which is re-exported through tonic.
87        let reason = {
88            use tonic_types::StatusExt;
89            status.get_details_error_info().map(|info| info.reason)
90        };
91        A2aClientError::Grpc(status.code(), status.message().to_string(), reason)
92    }
93}
94
95#[cfg(feature = "grpc")]
96impl From<tonic::transport::Error> for A2aClientError {
97    fn from(err: tonic::transport::Error) -> Self {
98        A2aClientError::GrpcTransport(err.to_string())
99    }
100}