1use std::fmt;
9use std::time::{Duration, SystemTime};
10
11use http::header::HeaderMap;
12use serde_json::Value;
13
14use crate::constants::{
15 MAX_ERROR_BODY_LENGTH, REQUEST_ID_HEADER, RETRY_AFTER_HEADER, RETRY_AFTER_MS_HEADER,
16};
17
18pub type Result<T, E = Error> = std::result::Result<T, E>;
20
21#[derive(Debug, thiserror::Error)]
23#[non_exhaustive]
24pub enum Error {
25 #[error("{0}")]
28 Config(String),
29
30 #[error("{0}")]
33 InvalidRequest(String),
34
35 #[error(transparent)]
37 Api(Box<ApiError>),
38
39 #[error("Connection error: {0}")]
42 Connection(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
43
44 #[error("Request timed out (timeout={}s).", .0.as_secs_f64())]
46 Timeout(Duration),
47
48 #[error(transparent)]
50 ResponseValidation(Box<ResponseValidationError>),
51}
52
53impl Error {
54 pub fn as_api(&self) -> Option<&ApiError> {
56 match self {
57 Error::Api(e) => Some(e),
58 _ => None,
59 }
60 }
61
62 pub fn status(&self) -> Option<u16> {
64 match self {
65 Error::Api(e) => Some(e.status),
66 Error::ResponseValidation(e) => Some(e.status),
67 _ => None,
68 }
69 }
70
71 pub fn request_id(&self) -> Option<&str> {
73 match self {
74 Error::Api(e) => e.request_id(),
75 Error::ResponseValidation(e) => header_str(&e.headers, REQUEST_ID_HEADER),
76 _ => None,
77 }
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
83#[non_exhaustive]
84pub enum ApiErrorKind {
85 BadRequest,
87 Authentication,
89 PermissionDenied,
91 NotFound,
93 UnprocessableEntity,
95 RateLimit,
97 InternalServer,
99 Other,
101}
102
103impl ApiErrorKind {
104 pub fn from_status(status: u16) -> Self {
106 match status {
107 400 => Self::BadRequest,
108 401 => Self::Authentication,
109 403 => Self::PermissionDenied,
110 404 => Self::NotFound,
111 422 => Self::UnprocessableEntity,
112 429 => Self::RateLimit,
113 s if s >= 500 => Self::InternalServer,
114 _ => Self::Other,
115 }
116 }
117}
118
119#[derive(Debug, Clone)]
121#[non_exhaustive]
122pub struct ApiError {
123 pub status: u16,
125 pub kind: ApiErrorKind,
127 pub message: String,
129 pub body: Option<Value>,
131 pub headers: HeaderMap,
133 pub endpoint: Option<String>,
135}
136
137impl ApiError {
138 pub(crate) fn new(
139 status: u16,
140 body: Option<Value>,
141 headers: HeaderMap,
142 endpoint: Option<String>,
143 ) -> Self {
144 let message = match body.as_ref().and_then(extract_message) {
145 Some(m) => m,
146 None => match &body {
147 None => "status code (no body)".to_owned(),
148 Some(Value::String(s)) => truncate(s),
149 Some(v) => truncate(&v.to_string()),
150 },
151 };
152 Self {
153 status,
154 kind: ApiErrorKind::from_status(status),
155 message,
156 body,
157 headers,
158 endpoint,
159 }
160 }
161
162 pub fn request_id(&self) -> Option<&str> {
164 header_str(&self.headers, REQUEST_ID_HEADER)
165 }
166
167 pub fn retry_after(&self) -> Option<Duration> {
169 parse_retry_after(&self.headers)
170 }
171}
172
173impl fmt::Display for ApiError {
174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175 if let Some(endpoint) = &self.endpoint {
176 write!(f, "{endpoint}: ")?;
177 }
178 write!(f, "{} {}", self.status, self.message)?;
179 if let Some(id) = self.request_id() {
180 write!(f, " (request_id={id})")?;
181 }
182 Ok(())
183 }
184}
185
186impl std::error::Error for ApiError {}
187
188#[derive(Debug, Clone)]
190#[non_exhaustive]
191pub struct ResponseValidationError {
192 pub status: u16,
194 pub field_path: String,
196 pub detail: String,
198 pub body: Option<Value>,
200 pub headers: HeaderMap,
202 pub endpoint: Option<String>,
204}
205
206impl fmt::Display for ResponseValidationError {
207 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208 if let Some(endpoint) = &self.endpoint {
209 write!(f, "{endpoint}: ")?;
210 }
211 write!(
212 f,
213 "{} Invalid response data at '{}': {}",
214 self.status, self.field_path, self.detail
215 )?;
216 if let Some(id) = header_str(&self.headers, REQUEST_ID_HEADER) {
217 write!(f, " (request_id={id})")?;
218 }
219 Ok(())
220 }
221}
222
223impl std::error::Error for ResponseValidationError {}
224
225fn header_str<'a>(headers: &'a HeaderMap, name: &str) -> Option<&'a str> {
226 headers.get(name).and_then(|v| v.to_str().ok())
227}
228
229fn truncate(raw: &str) -> String {
230 if raw.chars().count() > MAX_ERROR_BODY_LENGTH {
231 let mut s: String = raw.chars().take(MAX_ERROR_BODY_LENGTH).collect();
232 s.push('…');
233 s
234 } else {
235 raw.to_owned()
236 }
237}
238
239pub(crate) fn lenient_body(bytes: &[u8]) -> Option<Value> {
241 if bytes.is_empty() {
242 return None;
243 }
244 Some(
245 serde_json::from_slice(bytes)
246 .unwrap_or_else(|_| Value::String(String::from_utf8_lossy(bytes).into_owned())),
247 )
248}
249
250pub(crate) fn extract_message(body: &Value) -> Option<String> {
252 let non_empty = |s: &str| (!s.is_empty()).then(|| s.to_owned());
253 let obj = match body {
254 Value::String(s) => return non_empty(s),
255 Value::Object(o) => o,
256 _ => return None,
257 };
258 let str_at = |v: Option<&Value>, key: &str| {
259 v.and_then(|v| v.get(key))
260 .and_then(Value::as_str)
261 .map(str::to_owned)
262 };
263 match obj.get("error") {
264 Some(Value::String(s)) => return Some(s.clone()),
265 e @ Some(Value::Object(_)) => {
266 if let Some(m) = str_at(e, "message") {
267 return Some(m);
268 }
269 }
270 _ => {}
271 }
272 if let Some(Value::String(m)) = obj.get("message") {
273 return Some(m.clone());
274 }
275 match obj.get("detail") {
276 Some(Value::String(s)) => Some(s.clone()),
277 d @ Some(Value::Object(_)) => str_at(d, "message"),
278 Some(Value::Array(entries)) => {
279 let parts: Vec<String> = entries
280 .iter()
281 .filter_map(|entry| {
282 let msg = entry.get("msg")?.as_str()?;
283 let path = entry
284 .get("loc")
285 .and_then(Value::as_array)
286 .map(|loc| {
287 loc.iter()
288 .filter(|item| item.as_str() != Some("body"))
289 .map(|item| match item {
290 Value::String(s) => s.clone(),
291 other => other.to_string(),
292 })
293 .collect::<Vec<_>>()
294 .join(".")
295 })
296 .unwrap_or_default();
297 Some(if path.is_empty() {
298 msg.to_owned()
299 } else {
300 format!("{path}: {msg}")
301 })
302 })
303 .collect();
304 (!parts.is_empty()).then(|| parts.join("; "))
305 }
306 _ => None,
307 }
308}
309
310pub(crate) fn parse_retry_after(headers: &HeaderMap) -> Option<Duration> {
312 if let Some(raw) = header_str(headers, RETRY_AFTER_MS_HEADER) {
313 let raw = raw.trim();
314 if let Ok(ms) = if raw.is_empty() {
315 Ok(0.0)
316 } else {
317 raw.parse::<f64>()
318 } && ms.is_finite()
319 && ms >= 0.0
320 {
321 return Some(Duration::from_secs_f64(ms / 1000.0));
322 }
323 }
324 let raw = header_str(headers, RETRY_AFTER_HEADER)?;
325 let trimmed = raw.trim();
326 match if trimmed.is_empty() {
327 Ok(0.0)
328 } else {
329 trimmed.parse::<f64>()
330 } {
331 Ok(secs) if secs.is_finite() && secs >= 0.0 => Duration::try_from_secs_f64(secs).ok(),
332 Ok(_) => None,
333 Err(_) => {
334 let at = httpdate::parse_http_date(trimmed).ok()?;
335 Some(
336 at.duration_since(SystemTime::now())
337 .unwrap_or(Duration::ZERO),
338 )
339 }
340 }
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346 use http::header::HeaderValue;
347 use serde_json::json;
348
349 #[test]
350 fn extracts_fastapi_validation_details() {
351 let body = json!({"detail": [
352 {"loc": ["body", "questions", "x", "criteria"], "msg": "Field required", "type": "missing"},
353 {"loc": ["body", "model"], "msg": "Bad model", "type": "value_error"}
354 ]});
355 assert_eq!(
356 extract_message(&body).unwrap(),
357 "questions.x.criteria: Field required; model: Bad model"
358 );
359 }
360
361 #[test]
362 fn extracts_message_precedence() {
363 assert_eq!(
364 extract_message(&json!({"error": "e", "message": "m"})).unwrap(),
365 "e"
366 );
367 assert_eq!(
368 extract_message(&json!({"error": {"message": "em"}})).unwrap(),
369 "em"
370 );
371 assert_eq!(
372 extract_message(&json!({"message": "m", "detail": "d"})).unwrap(),
373 "m"
374 );
375 assert_eq!(
376 extract_message(&json!({"detail": {"message": "dm"}})).unwrap(),
377 "dm"
378 );
379 assert_eq!(extract_message(&json!({"other": 1})), None);
380 assert_eq!(extract_message(&json!("")), None);
381 }
382
383 #[test]
384 fn long_bodies_are_truncated() {
385 let err = ApiError::new(
386 500,
387 Some(json!({"x": "y".repeat(500)})),
388 HeaderMap::new(),
389 None,
390 );
391 assert_eq!(err.message.chars().count(), MAX_ERROR_BODY_LENGTH + 1);
392 assert!(err.message.ends_with('…'));
393 }
394
395 #[test]
396 fn empty_body_message() {
397 let err = ApiError::new(
398 503,
399 None,
400 HeaderMap::new(),
401 Some("GET http://x/v1/models".into()),
402 );
403 assert_eq!(
404 err.to_string(),
405 "GET http://x/v1/models: 503 status code (no body)"
406 );
407 assert_eq!(err.kind, ApiErrorKind::InternalServer);
408 }
409
410 #[test]
411 fn retry_after_variants() {
412 let mut h = HeaderMap::new();
413 h.insert(RETRY_AFTER_MS_HEADER, HeaderValue::from_static("250"));
414 h.insert(RETRY_AFTER_HEADER, HeaderValue::from_static("9"));
415 assert_eq!(parse_retry_after(&h), Some(Duration::from_millis(250)));
416
417 let mut h = HeaderMap::new();
418 h.insert(RETRY_AFTER_HEADER, HeaderValue::from_static("2"));
419 assert_eq!(parse_retry_after(&h), Some(Duration::from_secs(2)));
420
421 let mut h = HeaderMap::new();
422 h.insert(RETRY_AFTER_HEADER, HeaderValue::from_static("-1"));
423 assert_eq!(parse_retry_after(&h), None);
424
425 let mut h = HeaderMap::new();
426 h.insert(
427 RETRY_AFTER_HEADER,
428 HeaderValue::from_static(" Wed, 21 Oct 2015 07:28:00 GMT "),
429 );
430 assert_eq!(parse_retry_after(&h), Some(Duration::ZERO));
431 }
432}