1use std::fmt;
4use std::time::Duration;
5
6use reqwest::StatusCode;
7use reqwest::header::HeaderMap;
8use serde_json::Value;
9
10use crate::retry::parse_retry_after;
11
12pub const REQUEST_ID_HEADER: &str = "x-typesafe-request-id";
14
15const MAX_RAW_BODY_IN_MESSAGE: usize = 200;
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}")]
27 Config(String),
28
29 #[error("{0}")]
31 InvalidRequest(String),
32
33 #[error(transparent)]
35 Api(Box<ApiError>),
36
37 #[error("Connection error: {source}")]
39 Connection {
40 #[source]
42 source: reqwest::Error,
43 },
44
45 #[error("Request timed out after {}ms.", .timeout.as_millis())]
47 Timeout {
48 timeout: Duration,
50 },
51
52 #[error("{message}")]
54 Decode {
55 message: String,
57 request_id: Option<String>,
59 #[source]
61 source: Option<serde_json::Error>,
62 },
63
64 #[error("Could not get credentials: {source}")]
68 Credentials {
69 #[source]
71 source: crate::credentials::BoxError,
72 },
73
74 #[error("Unexpected answer \"{name}\": {reason}.")]
76 UnexpectedAnswer {
77 name: String,
79 reason: String,
81 },
82}
83
84impl Error {
85 pub fn status(&self) -> Option<StatusCode> {
87 self.api_error().map(ApiError::status)
88 }
89
90 pub fn request_id(&self) -> Option<&str> {
92 match self {
93 Error::Api(err) => err.request_id(),
94 Error::Decode { request_id, .. } => request_id.as_deref(),
95 _ => None,
96 }
97 }
98
99 pub fn api_error(&self) -> Option<&ApiError> {
101 match self {
102 Error::Api(err) => Some(err),
103 _ => None,
104 }
105 }
106
107 pub fn is_timeout(&self) -> bool {
109 matches!(self, Error::Timeout { .. })
110 }
111
112 pub fn is_connection(&self) -> bool {
114 matches!(self, Error::Connection { .. } | Error::Timeout { .. })
115 }
116}
117
118impl From<ApiError> for Error {
119 fn from(err: ApiError) -> Self {
120 Error::Api(Box::new(err))
121 }
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
126#[non_exhaustive]
127pub enum ApiErrorKind {
128 BadRequest,
130 Authentication,
132 PermissionDenied,
134 NotFound,
136 UnprocessableEntity,
138 RateLimit,
140 Overloaded,
143 InternalServer,
145 Other,
147}
148
149impl ApiErrorKind {
150 pub fn from_status(status: StatusCode) -> Self {
152 match status.as_u16() {
153 400 => Self::BadRequest,
154 401 => Self::Authentication,
155 403 => Self::PermissionDenied,
156 404 => Self::NotFound,
157 422 => Self::UnprocessableEntity,
158 429 => Self::RateLimit,
159 529 => Self::Overloaded,
160 500.. => Self::InternalServer,
161 _ => Self::Other,
162 }
163 }
164}
165
166#[derive(Debug, Clone, PartialEq)]
168pub enum ErrorBody {
169 Empty,
171 Json(Value),
173 Text(String),
175}
176
177impl ErrorBody {
178 pub fn parse(bytes: &[u8]) -> Self {
182 if bytes.is_empty() {
183 return Self::Empty;
184 }
185 match serde_json::from_slice(bytes) {
186 Ok(value) => Self::Json(value),
187 Err(_) => Self::Text(String::from_utf8_lossy(bytes).into_owned()),
188 }
189 }
190}
191
192#[derive(Debug, Clone)]
194pub struct ApiError {
195 kind: ApiErrorKind,
196 status: StatusCode,
197 headers: HeaderMap,
198 body: ErrorBody,
199 request_id: Option<String>,
200 message: String,
201}
202
203impl ApiError {
204 pub fn from_response(status: StatusCode, headers: HeaderMap, body: &[u8]) -> Self {
206 let body = ErrorBody::parse(body);
207 let message = describe(status, &body);
208 Self {
209 kind: ApiErrorKind::from_status(status),
210 request_id: request_id_from(&headers),
211 status,
212 headers,
213 body,
214 message,
215 }
216 }
217
218 pub fn kind(&self) -> ApiErrorKind {
220 self.kind
221 }
222
223 pub fn status(&self) -> StatusCode {
225 self.status
226 }
227
228 pub fn headers(&self) -> &HeaderMap {
230 &self.headers
231 }
232
233 pub fn body(&self) -> &ErrorBody {
235 &self.body
236 }
237
238 pub fn request_id(&self) -> Option<&str> {
240 self.request_id.as_deref()
241 }
242
243 pub fn retry_after(&self) -> Option<Duration> {
247 parse_retry_after(&self.headers, std::time::SystemTime::now())
248 }
249
250 pub fn message(&self) -> &str {
252 &self.message
253 }
254}
255
256impl fmt::Display for ApiError {
257 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258 f.write_str(&self.message)
259 }
260}
261
262impl std::error::Error for ApiError {}
263
264pub(crate) fn request_id_from(headers: &HeaderMap) -> Option<String> {
265 [REQUEST_ID_HEADER, "cf-ray", "x-request-id", "x-vercel-id"]
266 .into_iter()
267 .find_map(|name| headers.get(name))
268 .and_then(|value| value.to_str().ok())
269 .map(str::to_owned)
270}
271
272fn describe(status: StatusCode, body: &ErrorBody) -> String {
274 let status = status.as_u16();
275 if let Some(detail) = extract_message(body).filter(|detail| !detail.is_empty()) {
277 return format!("{status} {detail}");
278 }
279 let raw = match body {
280 ErrorBody::Empty => return format!("{status} status code (no body)"),
281 ErrorBody::Text(text) | ErrorBody::Json(Value::String(text)) => text.clone(),
282 ErrorBody::Json(value) => value.to_string(),
283 };
284 match raw.char_indices().nth(MAX_RAW_BODY_IN_MESSAGE) {
285 Some((cut, _)) => format!("{status} {}…", &raw[..cut]),
286 None => format!("{status} {raw}"),
287 }
288}
289
290fn extract_message(body: &ErrorBody) -> Option<String> {
292 let value = match body {
293 ErrorBody::Empty => return None,
294 ErrorBody::Text(text) => return non_empty(text),
295 ErrorBody::Json(value) => value,
296 };
297 if let Value::String(text) = value {
298 return non_empty(text);
299 }
300 let object = value.as_object()?;
301 let error = object.get("error");
302 let detail = object.get("detail");
303 if let Some(Value::String(text)) = error {
304 return Some(text.clone());
305 }
306 if let Some(text) = error.and_then(Value::as_object).and_then(nested_message) {
307 return Some(text);
308 }
309 if let Some(Value::String(text)) = object.get("message") {
310 return Some(text.clone());
311 }
312 match detail {
313 Some(Value::String(text)) => Some(text.clone()),
314 Some(Value::Object(inner)) => nested_message(inner),
315 Some(Value::Array(errors)) => describe_validation_errors(errors),
316 _ => None,
317 }
318}
319
320fn nested_message(object: &serde_json::Map<String, Value>) -> Option<String> {
327 ["message", "error_type"]
328 .into_iter()
329 .find_map(|key| object.get(key).and_then(Value::as_str))
330 .and_then(non_empty)
331}
332
333fn non_empty(text: &str) -> Option<String> {
334 (!text.is_empty()).then(|| text.to_owned())
335}
336
337fn describe_validation_errors(errors: &[Value]) -> Option<String> {
339 let parts: Vec<String> = errors
340 .iter()
341 .filter_map(|error| {
342 let msg = error.get("msg")?.as_str()?;
343 let loc = error
344 .get("loc")
345 .and_then(Value::as_array)
346 .map(|segments| {
347 segments
348 .iter()
349 .filter(|segment| segment.as_str() != Some("body"))
350 .map(loc_segment)
351 .collect::<Vec<_>>()
352 .join(".")
353 })
354 .unwrap_or_default();
355 Some(if loc.is_empty() {
356 msg.to_owned()
357 } else {
358 format!("{loc}: {msg}")
359 })
360 })
361 .collect();
362 (!parts.is_empty()).then(|| parts.join("; "))
363}
364
365fn loc_segment(segment: &Value) -> String {
366 match segment {
367 Value::String(text) => text.clone(),
368 Value::Null => String::new(),
369 other => other.to_string(),
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376 use reqwest::header::HeaderValue;
377 use serde_json::json;
378
379 fn message(status: u16, body: &[u8]) -> String {
380 ApiError::from_response(
381 StatusCode::from_u16(status).unwrap(),
382 HeaderMap::new(),
383 body,
384 )
385 .to_string()
386 }
387
388 #[test]
389 fn classifies_statuses() {
390 let kind = |s| ApiErrorKind::from_status(StatusCode::from_u16(s).unwrap());
391 assert_eq!(kind(400), ApiErrorKind::BadRequest);
392 assert_eq!(kind(401), ApiErrorKind::Authentication);
393 assert_eq!(kind(403), ApiErrorKind::PermissionDenied);
394 assert_eq!(kind(404), ApiErrorKind::NotFound);
395 assert_eq!(kind(422), ApiErrorKind::UnprocessableEntity);
396 assert_eq!(kind(429), ApiErrorKind::RateLimit);
397 assert_eq!(kind(500), ApiErrorKind::InternalServer);
398 assert_eq!(kind(529), ApiErrorKind::Overloaded);
399 assert_eq!(kind(599), ApiErrorKind::InternalServer);
400 assert_eq!(kind(409), ApiErrorKind::Other);
401 assert_eq!(kind(302), ApiErrorKind::Other);
402 }
403
404 #[test]
405 fn extracts_messages_from_common_shapes() {
406 assert_eq!(message(400, br#"{"error":"bad"}"#), "400 bad");
407 assert_eq!(
408 message(400, br#"{"error":{"message":"nested"}}"#),
409 "400 nested"
410 );
411 assert_eq!(message(400, br#"{"message":"plain"}"#), "400 plain");
412 assert_eq!(message(400, br#"{"detail":"why"}"#), "400 why");
413 assert_eq!(
414 message(400, br#"{"detail":{"message":"deep"}}"#),
415 "400 deep"
416 );
417 assert_eq!(message(400, br#""quoted""#), "400 quoted");
418 assert_eq!(message(502, b"Bad Gateway"), "502 Bad Gateway");
419 }
420
421 #[test]
424 fn error_type_is_used_when_there_is_no_message() {
425 assert_eq!(
426 message(400, br#"{"detail":{"error_type":"max_tokens_exceeded"}}"#),
427 "400 max_tokens_exceeded"
428 );
429 assert_eq!(
430 message(400, br#"{"error":{"error_type":"rate_limited"}}"#),
431 "400 rate_limited"
432 );
433 assert_eq!(
435 message(
436 400,
437 br#"{"detail":{"error_type":"bad","message":"be specific"}}"#
438 ),
439 "400 be specific"
440 );
441 }
442
443 #[test]
444 fn prefers_error_over_message_and_detail() {
445 assert_eq!(
446 message(400, br#"{"detail":"d","message":"m","error":"e"}"#),
447 "400 e"
448 );
449 assert_eq!(message(400, br#"{"detail":"d","message":"m"}"#), "400 m");
450 }
451
452 #[test]
453 fn formats_validation_errors_without_the_body_prefix() {
454 let body = json!({"detail": [
455 {"loc": ["body", "questions", "x", 0], "msg": "too short"},
456 {"loc": [], "msg": "bad state"},
457 {"msg": 3},
458 ]});
459 assert_eq!(
460 message(422, body.to_string().as_bytes()),
461 "422 questions.x.0: too short; bad state"
462 );
463 }
464
465 #[test]
466 fn falls_back_to_raw_body_or_no_body() {
467 assert_eq!(message(500, b""), "500 status code (no body)");
468 assert_eq!(message(500, br#"{"other":1}"#), r#"500 {"other":1}"#);
469 assert_eq!(message(500, br#"{"detail":[]}"#), r#"500 {"detail":[]}"#);
470 }
471
472 #[test]
473 fn empty_extracted_messages_fall_back_to_the_raw_body() {
474 assert_eq!(
477 message(400, br#"{"error":"","message":"m"}"#),
478 r#"400 {"error":"","message":"m"}"#
479 );
480 assert_eq!(message(400, br#"{"message":""}"#), r#"400 {"message":""}"#);
481 assert_eq!(
482 message(400, br#"{"error":{"message":""}}"#),
483 r#"400 {"error":{"message":""}}"#
484 );
485 assert_eq!(message(400, br#"{"detail":""}"#), r#"400 {"detail":""}"#);
486 }
487
488 #[test]
489 fn json_string_bodies_are_not_quoted() {
490 assert_eq!(message(400, br#""""#), "400 ");
491 }
492
493 #[test]
494 fn truncates_long_json_bodies_on_char_boundaries() {
495 let body = json!({ "x": "é".repeat(250) }).to_string();
496 let expected: String = body.chars().take(200).collect();
497 assert_eq!(message(500, body.as_bytes()), format!("500 {expected}…"));
498
499 let exact = json!({ "x": "a".repeat(192) }).to_string();
500 assert_eq!(exact.chars().count(), 200);
501 assert_eq!(message(500, exact.as_bytes()), format!("500 {exact}"));
502 }
503
504 #[test]
505 fn text_bodies_are_the_message_in_full() {
506 let long = "é".repeat(250);
507 assert_eq!(message(502, long.as_bytes()), format!("502 {long}"));
508 }
509
510 #[test]
511 fn exposes_request_id_and_retry_after() {
512 let mut headers = HeaderMap::new();
513 headers.insert(REQUEST_ID_HEADER, HeaderValue::from_static("req_1"));
514 headers.insert("retry-after", HeaderValue::from_static("7"));
515 let err = ApiError::from_response(StatusCode::TOO_MANY_REQUESTS, headers, b"{}");
516 assert_eq!(err.request_id(), Some("req_1"));
517 assert_eq!(err.retry_after(), Some(Duration::from_secs(7)));
518 assert_eq!(err.kind(), ApiErrorKind::RateLimit);
519
520 let wrapped = Error::from(err);
521 assert_eq!(wrapped.request_id(), Some("req_1"));
522 assert_eq!(wrapped.status(), Some(StatusCode::TOO_MANY_REQUESTS));
523 assert!(!wrapped.is_connection());
524 }
525
526 #[test]
527 fn timeouts_are_connection_errors() {
528 let err = Error::Timeout {
529 timeout: Duration::from_millis(1000),
530 };
531 assert!(err.is_timeout());
532 assert!(err.is_connection());
533 assert_eq!(err.to_string(), "Request timed out after 1000ms.");
534 }
535}