1#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
11#[non_exhaustive]
12pub enum TesseraError {
13 #[error("{0}")]
15 Configuration(String),
16 #[error("{0}")]
18 InvalidArgument(String),
19 #[error("network error contacting Tessera: {0}")]
21 Network(String),
22 #[error(
24 "A presigned download URL was rejected (likely expired). Presigned URLs are short-lived — call read()/scan() again to mint fresh ones."
25 )]
26 PresignExpired,
27 #[error("{message}")]
29 BadRequest {
30 code: Option<String>,
32 message: String,
34 },
35 #[error("{message}")]
37 Authentication {
38 code: Option<String>,
40 message: String,
42 },
43 #[error("{message}")]
45 Forbidden {
46 code: Option<String>,
48 message: String,
50 },
51 #[error("{message}")]
53 NotFound {
54 code: Option<String>,
56 message: String,
58 },
59 #[error("{message}")]
61 ServiceUnavailable {
62 code: Option<String>,
64 message: String,
66 },
67 #[error("{message}")]
69 InternalServer {
70 code: Option<String>,
72 message: String,
74 },
75 #[error("{message}")]
77 Api {
78 status_code: u16,
80 code: Option<String>,
82 message: String,
84 },
85}
86
87impl TesseraError {
88 #[must_use]
90 pub fn status_code(&self) -> Option<u16> {
91 match self {
92 Self::BadRequest { .. } => Some(400),
93 Self::Authentication { .. } => Some(401),
94 Self::Forbidden { .. } => Some(403),
95 Self::NotFound { .. } => Some(404),
96 Self::ServiceUnavailable { .. } => Some(503),
97 Self::InternalServer { .. } => Some(500),
98 Self::Api { status_code, .. } => Some(*status_code),
99 Self::Configuration(_)
100 | Self::InvalidArgument(_)
101 | Self::Network(_)
102 | Self::PresignExpired => None,
103 }
104 }
105
106 #[must_use]
108 pub fn code(&self) -> Option<&str> {
109 match self {
110 Self::BadRequest { code, .. }
111 | Self::Authentication { code, .. }
112 | Self::Forbidden { code, .. }
113 | Self::NotFound { code, .. }
114 | Self::ServiceUnavailable { code, .. }
115 | Self::InternalServer { code, .. }
116 | Self::Api { code, .. } => code.as_deref(),
117 Self::Configuration(_)
118 | Self::InvalidArgument(_)
119 | Self::Network(_)
120 | Self::PresignExpired => None,
121 }
122 }
123}
124
125pub fn error_from_response(status_code: u16, body: &[u8]) -> TesseraError {
129 let code: Option<String> = serde_json::from_slice::<serde_json::Value>(body)
130 .ok()
131 .and_then(|parsed| {
132 parsed
133 .get("error")
134 .and_then(serde_json::Value::as_str)
135 .map(ToOwned::to_owned)
136 });
137
138 let detail = code
139 .as_ref()
140 .map_or_else(String::new, |c| format!(" ({c})"));
141 let message = format!("Tessera API request failed with HTTP {status_code}{detail}");
142
143 if let Some(c) = &code {
144 return match c.as_str() {
145 "bad_request" => TesseraError::BadRequest {
146 code: Some(c.clone()),
147 message,
148 },
149 "unauthorized" => TesseraError::Authentication {
150 code: Some(c.clone()),
151 message,
152 },
153 "forbidden" => TesseraError::Forbidden {
154 code: Some(c.clone()),
155 message,
156 },
157 "not_found" => TesseraError::NotFound {
158 code: Some(c.clone()),
159 message,
160 },
161 "unavailable" => TesseraError::ServiceUnavailable {
162 code: Some(c.clone()),
163 message,
164 },
165 "internal" => TesseraError::InternalServer {
166 code: Some(c.clone()),
167 message,
168 },
169 _ => TesseraError::Api {
170 status_code,
171 code: Some(c.clone()),
172 message,
173 },
174 };
175 }
176 match status_code {
177 400 => TesseraError::BadRequest {
178 code: None,
179 message,
180 },
181 401 => TesseraError::Authentication {
182 code: None,
183 message,
184 },
185 403 => TesseraError::Forbidden {
186 code: None,
187 message,
188 },
189 404 => TesseraError::NotFound {
190 code: None,
191 message,
192 },
193 500 => TesseraError::InternalServer {
194 code: None,
195 message,
196 },
197 502..=504 => TesseraError::ServiceUnavailable {
198 code: None,
199 message,
200 },
201 _ => TesseraError::Api {
202 status_code,
203 code: None,
204 message,
205 },
206 }
207}