1use crate::xrpc::EncodeError;
4use bytes::Bytes;
5
6#[derive(Debug, thiserror::Error, miette::Diagnostic)]
8pub enum ClientError {
9 #[error("HTTP transport error: {0}")]
11 Transport(
12 #[from]
13 #[diagnostic_source]
14 TransportError,
15 ),
16
17 #[error("{0}")]
19 Encode(
20 #[from]
21 #[diagnostic_source]
22 EncodeError,
23 ),
24
25 #[error("{0}")]
27 Decode(
28 #[from]
29 #[diagnostic_source]
30 DecodeError,
31 ),
32
33 #[error("HTTP {0}")]
35 Http(
36 #[from]
37 #[diagnostic_source]
38 HttpError,
39 ),
40
41 #[error("Authentication error: {0}")]
43 Auth(
44 #[from]
45 #[diagnostic_source]
46 AuthError,
47 ),
48}
49
50#[derive(Debug, thiserror::Error, miette::Diagnostic)]
52pub enum TransportError {
53 #[error("Connection error: {0}")]
55 Connect(String),
56
57 #[error("Request timeout")]
59 Timeout,
60
61 #[error("Invalid request: {0}")]
63 InvalidRequest(String),
64
65 #[error("Transport error: {0}")]
67 Other(Box<dyn std::error::Error + Send + Sync>),
68}
69
70#[derive(Debug, thiserror::Error, miette::Diagnostic)]
72pub enum DecodeError {
73 #[error("Failed to deserialize JSON: {0}")]
75 Json(
76 #[from]
77 #[source]
78 serde_json::Error,
79 ),
80 #[error("Failed to deserialize CBOR: {0}")]
82 CborLocal(
83 #[from]
84 #[source]
85 serde_ipld_dagcbor::DecodeError<std::io::Error>,
86 ),
87 #[error("Failed to deserialize CBOR: {0}")]
89 CborRemote(
90 #[from]
91 #[source]
92 serde_ipld_dagcbor::DecodeError<HttpError>,
93 ),
94}
95
96#[derive(Debug, thiserror::Error, miette::Diagnostic)]
98pub struct HttpError {
99 pub status: http::StatusCode,
101 pub body: Option<Bytes>,
103}
104
105impl std::fmt::Display for HttpError {
106 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 write!(f, "HTTP {}", self.status)?;
108 if let Some(body) = &self.body {
109 if let Ok(s) = std::str::from_utf8(body) {
110 write!(f, ":\n{}", s)?;
111 }
112 }
113 Ok(())
114 }
115}
116
117pub type XrpcResult<T> = std::result::Result<T, ClientError>;
119
120#[cfg(feature = "reqwest-client")]
121impl From<reqwest::Error> for TransportError {
122 fn from(e: reqwest::Error) -> Self {
123 if e.is_timeout() {
124 Self::Timeout
125 } else if e.is_connect() {
126 Self::Connect(e.to_string())
127 } else if e.is_builder() || e.is_request() {
128 Self::InvalidRequest(e.to_string())
129 } else {
130 Self::Other(Box::new(e))
131 }
132 }
133}
134
135#[derive(Debug, thiserror::Error, miette::Diagnostic)]
137pub enum AuthError {
138 #[error("Access token expired")]
140 TokenExpired,
141
142 #[error("Invalid access token")]
144 InvalidToken,
145
146 #[error("Token refresh failed")]
148 RefreshFailed,
149
150 #[error("No authentication provided, but endpoint requires auth")]
152 NotAuthenticated,
153
154 #[error("Authentication error: {0:?}")]
156 Other(http::HeaderValue),
157}
158
159impl crate::IntoStatic for AuthError {
160 type Output = AuthError;
161
162 fn into_static(self) -> Self::Output {
163 match self {
164 AuthError::TokenExpired => AuthError::TokenExpired,
165 AuthError::InvalidToken => AuthError::InvalidToken,
166 AuthError::RefreshFailed => AuthError::RefreshFailed,
167 AuthError::NotAuthenticated => AuthError::NotAuthenticated,
168 AuthError::Other(header) => AuthError::Other(header),
169 }
170 }
171}