1use std::fmt;
9use std::time::{Duration, SystemTime};
10
11use http::StatusCode;
12use http::header::HeaderMap;
13use serde_json::Value;
14
15use crate::constants::{
16 MAX_ERROR_BODY_LENGTH, REQUEST_ID_HEADER, RETRY_AFTER_HEADER, RETRY_AFTER_MS_HEADER,
17};
18
19pub type Result<T, E = Error> = std::result::Result<T, E>;
21
22pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
25
26#[derive(Debug, thiserror::Error)]
31#[non_exhaustive]
32pub enum Error {
33 #[error("{message}")]
37 Config {
38 message: String,
40 #[source]
42 source: Option<BoxError>,
43 },
44
45 #[error("{message}")]
48 InvalidRequest {
49 message: String,
51 #[source]
53 source: Option<BoxError>,
54 },
55
56 #[error(transparent)]
58 Api(Box<ApiError>),
59
60 #[error("connection error")]
63 Connection(#[source] BoxError),
64
65 #[error("request timed out (timeout={}s)", .0.as_secs_f64())]
67 Timeout(Duration),
68
69 #[error(transparent)]
71 ResponseValidation(Box<ResponseValidationError>),
72
73 #[error("no recording for this request: {} does not exist (replaying, so nothing was sent)", path.display())]
77 ReplayMiss {
78 key: String,
80 path: std::path::PathBuf,
82 },
83}
84
85impl Error {
86 pub(crate) fn config(message: impl Into<String>) -> Self {
88 Error::Config {
89 message: message.into(),
90 source: None,
91 }
92 }
93
94 pub(crate) fn config_caused(message: impl Into<String>, source: impl Into<BoxError>) -> Self {
96 Error::Config {
97 message: message.into(),
98 source: Some(source.into()),
99 }
100 }
101
102 pub(crate) fn invalid_request(message: impl Into<String>) -> Self {
104 Error::InvalidRequest {
105 message: message.into(),
106 source: None,
107 }
108 }
109
110 pub(crate) fn invalid_request_caused(
112 message: impl Into<String>,
113 source: impl Into<BoxError>,
114 ) -> Self {
115 Error::InvalidRequest {
116 message: message.into(),
117 source: Some(source.into()),
118 }
119 }
120
121 pub fn as_api(&self) -> Option<&ApiError> {
123 match self {
124 Error::Api(e) => Some(e),
125 _ => None,
126 }
127 }
128
129 pub fn status(&self) -> Option<StatusCode> {
139 match self {
140 Error::Api(e) => Some(e.status),
141 Error::ResponseValidation(e) => Some(e.status),
142 _ => None,
143 }
144 }
145
146 pub fn request_id(&self) -> Option<&str> {
148 match self {
149 Error::Api(e) => e.request_id(),
150 Error::ResponseValidation(e) => header_str(&e.headers, REQUEST_ID_HEADER),
151 _ => None,
152 }
153 }
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
158#[non_exhaustive]
159pub enum ApiErrorKind {
160 BadRequest,
162 Authentication,
164 PermissionDenied,
166 NotFound,
168 UnprocessableEntity,
170 RateLimit,
172 InternalServer,
174 Other,
176}
177
178impl ApiErrorKind {
179 pub fn from_status(status: StatusCode) -> Self {
181 match status {
182 StatusCode::BAD_REQUEST => Self::BadRequest,
183 StatusCode::UNAUTHORIZED => Self::Authentication,
184 StatusCode::FORBIDDEN => Self::PermissionDenied,
185 StatusCode::NOT_FOUND => Self::NotFound,
186 StatusCode::UNPROCESSABLE_ENTITY => Self::UnprocessableEntity,
187 StatusCode::TOO_MANY_REQUESTS => Self::RateLimit,
188 s if s.is_server_error() => Self::InternalServer,
189 _ => Self::Other,
190 }
191 }
192}
193
194#[derive(Debug, Clone)]
196#[non_exhaustive]
197pub struct ApiError {
198 pub status: StatusCode,
200 pub kind: ApiErrorKind,
202 pub message: String,
204 pub body: Option<Value>,
206 pub headers: HeaderMap,
208 pub endpoint: Option<String>,
210}
211
212impl ApiError {
213 pub(crate) fn new(
214 status: StatusCode,
215 body: Option<Value>,
216 headers: HeaderMap,
217 endpoint: Option<String>,
218 ) -> Self {
219 let message = match body.as_ref().and_then(extract_message) {
220 Some(m) => m,
221 None => match &body {
222 None => "status code (no body)".to_owned(),
223 Some(Value::String(s)) => truncate(s),
224 Some(v) => truncate(&v.to_string()),
225 },
226 };
227 Self {
228 status,
229 kind: ApiErrorKind::from_status(status),
230 message,
231 body,
232 headers,
233 endpoint,
234 }
235 }
236
237 pub fn request_id(&self) -> Option<&str> {
239 header_str(&self.headers, REQUEST_ID_HEADER)
240 }
241
242 pub fn retry_after(&self) -> Option<Duration> {
244 parse_retry_after(&self.headers)
245 }
246}
247
248impl fmt::Display for ApiError {
249 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250 if let Some(endpoint) = &self.endpoint {
251 write!(f, "{endpoint}: ")?;
252 }
253 write!(f, "{} {}", self.status.as_u16(), self.message)?;
254 if let Some(id) = self.request_id() {
255 write!(f, " (request_id={id})")?;
256 }
257 Ok(())
258 }
259}
260
261impl std::error::Error for ApiError {}
262
263#[derive(Debug, Clone)]
265#[non_exhaustive]
266pub struct ResponseValidationError {
267 pub status: StatusCode,
269 pub field_path: String,
271 pub detail: String,
273 pub body: Option<Value>,
275 pub headers: HeaderMap,
277 pub endpoint: Option<String>,
279}
280
281impl fmt::Display for ResponseValidationError {
282 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
283 if let Some(endpoint) = &self.endpoint {
284 write!(f, "{endpoint}: ")?;
285 }
286 write!(
287 f,
288 "{} invalid response data at '{}': {}",
289 self.status.as_u16(),
290 self.field_path,
291 self.detail
292 )?;
293 if let Some(id) = header_str(&self.headers, REQUEST_ID_HEADER) {
294 write!(f, " (request_id={id})")?;
295 }
296 Ok(())
297 }
298}
299
300impl std::error::Error for ResponseValidationError {}
301
302fn header_str<'a>(headers: &'a HeaderMap, name: &str) -> Option<&'a str> {
303 headers.get(name).and_then(|v| v.to_str().ok())
304}
305
306fn truncate(raw: &str) -> String {
307 if raw.chars().count() > MAX_ERROR_BODY_LENGTH {
308 let mut s: String = raw.chars().take(MAX_ERROR_BODY_LENGTH).collect();
309 s.push('…');
310 s
311 } else {
312 raw.to_owned()
313 }
314}
315
316pub(crate) fn lenient_body(bytes: &[u8]) -> Option<Value> {
318 if bytes.is_empty() {
319 return None;
320 }
321 Some(
322 serde_json::from_slice(bytes)
323 .unwrap_or_else(|_| Value::String(String::from_utf8_lossy(bytes).into_owned())),
324 )
325}
326
327pub(crate) fn extract_message(body: &Value) -> Option<String> {
329 let non_empty = |s: &str| (!s.is_empty()).then(|| s.to_owned());
330 let obj = match body {
331 Value::String(s) => return non_empty(s),
332 Value::Object(o) => o,
333 _ => return None,
334 };
335 let str_at = |v: Option<&Value>, key: &str| {
336 v.and_then(|v| v.get(key))
337 .and_then(Value::as_str)
338 .map(str::to_owned)
339 };
340 match obj.get("error") {
341 Some(Value::String(s)) => return Some(s.clone()),
342 e @ Some(Value::Object(_)) => {
343 if let Some(m) = str_at(e, "message") {
344 return Some(m);
345 }
346 }
347 _ => {}
348 }
349 if let Some(Value::String(m)) = obj.get("message") {
350 return Some(m.clone());
351 }
352 match obj.get("detail") {
353 Some(Value::String(s)) => Some(s.clone()),
354 d @ Some(Value::Object(_)) => str_at(d, "message"),
355 Some(Value::Array(entries)) => {
356 let parts: Vec<String> = entries
357 .iter()
358 .filter_map(|entry| {
359 let msg = entry.get("msg")?.as_str()?;
360 let path = entry
361 .get("loc")
362 .and_then(Value::as_array)
363 .map(|loc| {
364 loc.iter()
365 .filter(|item| item.as_str() != Some("body"))
366 .map(|item| match item {
367 Value::String(s) => s.clone(),
368 other => other.to_string(),
369 })
370 .collect::<Vec<_>>()
371 .join(".")
372 })
373 .unwrap_or_default();
374 Some(if path.is_empty() {
375 msg.to_owned()
376 } else {
377 format!("{path}: {msg}")
378 })
379 })
380 .collect();
381 (!parts.is_empty()).then(|| parts.join("; "))
382 }
383 _ => None,
384 }
385}
386
387pub(crate) fn parse_retry_after(headers: &HeaderMap) -> Option<Duration> {
389 if let Some(raw) = header_str(headers, RETRY_AFTER_MS_HEADER) {
390 let raw = raw.trim();
391 if let Ok(ms) = if raw.is_empty() {
392 Ok(0.0)
393 } else {
394 raw.parse::<f64>()
395 } && ms.is_finite()
396 && ms >= 0.0
397 && let Ok(delay) = Duration::try_from_secs_f64(ms / 1000.0)
398 {
399 return Some(delay);
400 }
401 }
402 let raw = header_str(headers, RETRY_AFTER_HEADER)?;
403 let trimmed = raw.trim();
404 match if trimmed.is_empty() {
405 Ok(0.0)
406 } else {
407 trimmed.parse::<f64>()
408 } {
409 Ok(secs) if secs.is_finite() && secs >= 0.0 => Duration::try_from_secs_f64(secs).ok(),
410 Ok(_) => None,
411 Err(_) => {
412 let at = httpdate::parse_http_date(trimmed).ok()?;
413 Some(
414 at.duration_since(SystemTime::now())
415 .unwrap_or(Duration::ZERO),
416 )
417 }
418 }
419}
420
421#[cfg(test)]
422mod tests {
423 use super::*;
424 use http::header::HeaderValue;
425 use serde_json::json;
426
427 #[test]
428 fn extracts_fastapi_validation_details() {
429 let body = json!({"detail": [
430 {"loc": ["body", "questions", "x", "criteria"], "msg": "Field required", "type": "missing"},
431 {"loc": ["body", "model"], "msg": "Bad model", "type": "value_error"}
432 ]});
433 assert_eq!(
434 extract_message(&body).unwrap(),
435 "questions.x.criteria: Field required; model: Bad model"
436 );
437 }
438
439 #[test]
440 fn extracts_message_precedence() {
441 assert_eq!(
442 extract_message(&json!({"error": "e", "message": "m"})).unwrap(),
443 "e"
444 );
445 assert_eq!(
446 extract_message(&json!({"error": {"message": "em"}})).unwrap(),
447 "em"
448 );
449 assert_eq!(
450 extract_message(&json!({"message": "m", "detail": "d"})).unwrap(),
451 "m"
452 );
453 assert_eq!(
454 extract_message(&json!({"detail": {"message": "dm"}})).unwrap(),
455 "dm"
456 );
457 assert_eq!(extract_message(&json!({"other": 1})), None);
458 assert_eq!(extract_message(&json!("")), None);
459 }
460
461 #[test]
462 fn long_bodies_are_truncated() {
463 let err = ApiError::new(
464 StatusCode::INTERNAL_SERVER_ERROR,
465 Some(json!({"x": "y".repeat(500)})),
466 HeaderMap::new(),
467 None,
468 );
469 assert_eq!(err.message.chars().count(), MAX_ERROR_BODY_LENGTH + 1);
470 assert!(err.message.ends_with('…'));
471 }
472
473 #[test]
474 fn empty_body_message() {
475 let err = ApiError::new(
476 StatusCode::SERVICE_UNAVAILABLE,
477 None,
478 HeaderMap::new(),
479 Some("GET http://x/v1/models".into()),
480 );
481 assert_eq!(
482 err.to_string(),
483 "GET http://x/v1/models: 503 status code (no body)"
484 );
485 assert_eq!(err.kind, ApiErrorKind::InternalServer);
486 }
487
488 #[test]
489 fn retry_after_variants() {
490 let mut h = HeaderMap::new();
491 h.insert(RETRY_AFTER_MS_HEADER, HeaderValue::from_static("250"));
492 h.insert(RETRY_AFTER_HEADER, HeaderValue::from_static("9"));
493 assert_eq!(parse_retry_after(&h), Some(Duration::from_millis(250)));
494
495 let mut h = HeaderMap::new();
496 h.insert(RETRY_AFTER_HEADER, HeaderValue::from_static("2"));
497 assert_eq!(parse_retry_after(&h), Some(Duration::from_secs(2)));
498
499 let mut h = HeaderMap::new();
500 h.insert(RETRY_AFTER_HEADER, HeaderValue::from_static("-1"));
501 assert_eq!(parse_retry_after(&h), None);
502
503 let mut h = HeaderMap::new();
504 h.insert(
505 RETRY_AFTER_HEADER,
506 HeaderValue::from_static(" Wed, 21 Oct 2015 07:28:00 GMT "),
507 );
508 assert_eq!(parse_retry_after(&h), Some(Duration::ZERO));
509
510 let mut h = HeaderMap::new();
512 h.insert(RETRY_AFTER_MS_HEADER, HeaderValue::from_static("1e300"));
513 assert_eq!(parse_retry_after(&h), None);
514 h.insert(RETRY_AFTER_HEADER, HeaderValue::from_static("3"));
515 assert_eq!(parse_retry_after(&h), Some(Duration::from_secs(3)));
516 }
517}