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 #[error("No recording for this request: {} does not exist (replaying, so nothing was sent).", path.display())]
56 ReplayMiss {
57 key: String,
59 path: std::path::PathBuf,
61 },
62}
63
64impl Error {
65 pub fn as_api(&self) -> Option<&ApiError> {
67 match self {
68 Error::Api(e) => Some(e),
69 _ => None,
70 }
71 }
72
73 pub fn status(&self) -> Option<u16> {
75 match self {
76 Error::Api(e) => Some(e.status),
77 Error::ResponseValidation(e) => Some(e.status),
78 _ => None,
79 }
80 }
81
82 pub fn request_id(&self) -> Option<&str> {
84 match self {
85 Error::Api(e) => e.request_id(),
86 Error::ResponseValidation(e) => header_str(&e.headers, REQUEST_ID_HEADER),
87 _ => None,
88 }
89 }
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
94#[non_exhaustive]
95pub enum ApiErrorKind {
96 BadRequest,
98 Authentication,
100 PermissionDenied,
102 NotFound,
104 UnprocessableEntity,
106 RateLimit,
108 InternalServer,
110 Other,
112}
113
114impl ApiErrorKind {
115 pub fn from_status(status: u16) -> Self {
117 match status {
118 400 => Self::BadRequest,
119 401 => Self::Authentication,
120 403 => Self::PermissionDenied,
121 404 => Self::NotFound,
122 422 => Self::UnprocessableEntity,
123 429 => Self::RateLimit,
124 s if s >= 500 => Self::InternalServer,
125 _ => Self::Other,
126 }
127 }
128}
129
130#[derive(Debug, Clone)]
132#[non_exhaustive]
133pub struct ApiError {
134 pub status: u16,
136 pub kind: ApiErrorKind,
138 pub message: String,
140 pub body: Option<Value>,
142 pub headers: HeaderMap,
144 pub endpoint: Option<String>,
146}
147
148impl ApiError {
149 pub(crate) fn new(
150 status: u16,
151 body: Option<Value>,
152 headers: HeaderMap,
153 endpoint: Option<String>,
154 ) -> Self {
155 let message = match body.as_ref().and_then(extract_message) {
156 Some(m) => m,
157 None => match &body {
158 None => "status code (no body)".to_owned(),
159 Some(Value::String(s)) => truncate(s),
160 Some(v) => truncate(&v.to_string()),
161 },
162 };
163 Self {
164 status,
165 kind: ApiErrorKind::from_status(status),
166 message,
167 body,
168 headers,
169 endpoint,
170 }
171 }
172
173 pub fn request_id(&self) -> Option<&str> {
175 header_str(&self.headers, REQUEST_ID_HEADER)
176 }
177
178 pub fn retry_after(&self) -> Option<Duration> {
180 parse_retry_after(&self.headers)
181 }
182}
183
184impl fmt::Display for ApiError {
185 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186 if let Some(endpoint) = &self.endpoint {
187 write!(f, "{endpoint}: ")?;
188 }
189 write!(f, "{} {}", self.status, self.message)?;
190 if let Some(id) = self.request_id() {
191 write!(f, " (request_id={id})")?;
192 }
193 Ok(())
194 }
195}
196
197impl std::error::Error for ApiError {}
198
199#[derive(Debug, Clone)]
201#[non_exhaustive]
202pub struct ResponseValidationError {
203 pub status: u16,
205 pub field_path: String,
207 pub detail: String,
209 pub body: Option<Value>,
211 pub headers: HeaderMap,
213 pub endpoint: Option<String>,
215}
216
217impl fmt::Display for ResponseValidationError {
218 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219 if let Some(endpoint) = &self.endpoint {
220 write!(f, "{endpoint}: ")?;
221 }
222 write!(
223 f,
224 "{} Invalid response data at '{}': {}",
225 self.status, self.field_path, self.detail
226 )?;
227 if let Some(id) = header_str(&self.headers, REQUEST_ID_HEADER) {
228 write!(f, " (request_id={id})")?;
229 }
230 Ok(())
231 }
232}
233
234impl std::error::Error for ResponseValidationError {}
235
236fn header_str<'a>(headers: &'a HeaderMap, name: &str) -> Option<&'a str> {
237 headers.get(name).and_then(|v| v.to_str().ok())
238}
239
240fn truncate(raw: &str) -> String {
241 if raw.chars().count() > MAX_ERROR_BODY_LENGTH {
242 let mut s: String = raw.chars().take(MAX_ERROR_BODY_LENGTH).collect();
243 s.push('…');
244 s
245 } else {
246 raw.to_owned()
247 }
248}
249
250pub(crate) fn lenient_body(bytes: &[u8]) -> Option<Value> {
252 if bytes.is_empty() {
253 return None;
254 }
255 Some(
256 serde_json::from_slice(bytes)
257 .unwrap_or_else(|_| Value::String(String::from_utf8_lossy(bytes).into_owned())),
258 )
259}
260
261pub(crate) fn extract_message(body: &Value) -> Option<String> {
263 let non_empty = |s: &str| (!s.is_empty()).then(|| s.to_owned());
264 let obj = match body {
265 Value::String(s) => return non_empty(s),
266 Value::Object(o) => o,
267 _ => return None,
268 };
269 let str_at = |v: Option<&Value>, key: &str| {
270 v.and_then(|v| v.get(key))
271 .and_then(Value::as_str)
272 .map(str::to_owned)
273 };
274 match obj.get("error") {
275 Some(Value::String(s)) => return Some(s.clone()),
276 e @ Some(Value::Object(_)) => {
277 if let Some(m) = str_at(e, "message") {
278 return Some(m);
279 }
280 }
281 _ => {}
282 }
283 if let Some(Value::String(m)) = obj.get("message") {
284 return Some(m.clone());
285 }
286 match obj.get("detail") {
287 Some(Value::String(s)) => Some(s.clone()),
288 d @ Some(Value::Object(_)) => str_at(d, "message"),
289 Some(Value::Array(entries)) => {
290 let parts: Vec<String> = entries
291 .iter()
292 .filter_map(|entry| {
293 let msg = entry.get("msg")?.as_str()?;
294 let path = entry
295 .get("loc")
296 .and_then(Value::as_array)
297 .map(|loc| {
298 loc.iter()
299 .filter(|item| item.as_str() != Some("body"))
300 .map(|item| match item {
301 Value::String(s) => s.clone(),
302 other => other.to_string(),
303 })
304 .collect::<Vec<_>>()
305 .join(".")
306 })
307 .unwrap_or_default();
308 Some(if path.is_empty() {
309 msg.to_owned()
310 } else {
311 format!("{path}: {msg}")
312 })
313 })
314 .collect();
315 (!parts.is_empty()).then(|| parts.join("; "))
316 }
317 _ => None,
318 }
319}
320
321pub(crate) fn parse_retry_after(headers: &HeaderMap) -> Option<Duration> {
323 if let Some(raw) = header_str(headers, RETRY_AFTER_MS_HEADER) {
324 let raw = raw.trim();
325 if let Ok(ms) = if raw.is_empty() {
326 Ok(0.0)
327 } else {
328 raw.parse::<f64>()
329 } && ms.is_finite()
330 && ms >= 0.0
331 {
332 return Some(Duration::from_secs_f64(ms / 1000.0));
333 }
334 }
335 let raw = header_str(headers, RETRY_AFTER_HEADER)?;
336 let trimmed = raw.trim();
337 match if trimmed.is_empty() {
338 Ok(0.0)
339 } else {
340 trimmed.parse::<f64>()
341 } {
342 Ok(secs) if secs.is_finite() && secs >= 0.0 => Duration::try_from_secs_f64(secs).ok(),
343 Ok(_) => None,
344 Err(_) => {
345 let at = httpdate::parse_http_date(trimmed).ok()?;
346 Some(
347 at.duration_since(SystemTime::now())
348 .unwrap_or(Duration::ZERO),
349 )
350 }
351 }
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357 use http::header::HeaderValue;
358 use serde_json::json;
359
360 #[test]
361 fn extracts_fastapi_validation_details() {
362 let body = json!({"detail": [
363 {"loc": ["body", "questions", "x", "criteria"], "msg": "Field required", "type": "missing"},
364 {"loc": ["body", "model"], "msg": "Bad model", "type": "value_error"}
365 ]});
366 assert_eq!(
367 extract_message(&body).unwrap(),
368 "questions.x.criteria: Field required; model: Bad model"
369 );
370 }
371
372 #[test]
373 fn extracts_message_precedence() {
374 assert_eq!(
375 extract_message(&json!({"error": "e", "message": "m"})).unwrap(),
376 "e"
377 );
378 assert_eq!(
379 extract_message(&json!({"error": {"message": "em"}})).unwrap(),
380 "em"
381 );
382 assert_eq!(
383 extract_message(&json!({"message": "m", "detail": "d"})).unwrap(),
384 "m"
385 );
386 assert_eq!(
387 extract_message(&json!({"detail": {"message": "dm"}})).unwrap(),
388 "dm"
389 );
390 assert_eq!(extract_message(&json!({"other": 1})), None);
391 assert_eq!(extract_message(&json!("")), None);
392 }
393
394 #[test]
395 fn long_bodies_are_truncated() {
396 let err = ApiError::new(
397 500,
398 Some(json!({"x": "y".repeat(500)})),
399 HeaderMap::new(),
400 None,
401 );
402 assert_eq!(err.message.chars().count(), MAX_ERROR_BODY_LENGTH + 1);
403 assert!(err.message.ends_with('…'));
404 }
405
406 #[test]
407 fn empty_body_message() {
408 let err = ApiError::new(
409 503,
410 None,
411 HeaderMap::new(),
412 Some("GET http://x/v1/models".into()),
413 );
414 assert_eq!(
415 err.to_string(),
416 "GET http://x/v1/models: 503 status code (no body)"
417 );
418 assert_eq!(err.kind, ApiErrorKind::InternalServer);
419 }
420
421 #[test]
422 fn retry_after_variants() {
423 let mut h = HeaderMap::new();
424 h.insert(RETRY_AFTER_MS_HEADER, HeaderValue::from_static("250"));
425 h.insert(RETRY_AFTER_HEADER, HeaderValue::from_static("9"));
426 assert_eq!(parse_retry_after(&h), Some(Duration::from_millis(250)));
427
428 let mut h = HeaderMap::new();
429 h.insert(RETRY_AFTER_HEADER, HeaderValue::from_static("2"));
430 assert_eq!(parse_retry_after(&h), Some(Duration::from_secs(2)));
431
432 let mut h = HeaderMap::new();
433 h.insert(RETRY_AFTER_HEADER, HeaderValue::from_static("-1"));
434 assert_eq!(parse_retry_after(&h), None);
435
436 let mut h = HeaderMap::new();
437 h.insert(
438 RETRY_AFTER_HEADER,
439 HeaderValue::from_static(" Wed, 21 Oct 2015 07:28:00 GMT "),
440 );
441 assert_eq!(parse_retry_after(&h), Some(Duration::ZERO));
442 }
443}