1use reqwest::StatusCode;
2use serde::{Deserialize, Serialize};
3use thiserror::Error;
4
5#[derive(Debug, Error)]
7pub enum ExaError {
8 #[error("HTTP error: {0}")]
10 Reqwest(#[from] reqwest::Error),
11
12 #[error("API error: {0:?}")]
14 Api(ApiErrorObject),
15
16 #[error("Invalid configuration: {0}")]
18 Config(String),
19
20 #[error("Serialization error: {0}")]
22 Serde(String),
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub struct ApiErrorObject {
29 #[serde(default)]
31 pub status_code: Option<u16>,
32 #[serde(default)]
34 pub message: String,
35 #[serde(default)]
37 pub timestamp: Option<String>,
38 #[serde(default)]
40 pub path: Option<String>,
41 #[serde(default)]
43 pub error: Option<String>,
44}
45
46impl ExaError {
47 #[must_use]
52 pub fn is_retryable(&self) -> bool {
53 match self {
54 Self::Api(obj) => obj
55 .status_code
56 .is_some_and(crate::retry::is_retryable_status),
57 Self::Reqwest(e) => e.is_timeout() || e.is_connect(),
58 Self::Config(_) | Self::Serde(_) => false,
59 }
60 }
61}
62
63#[must_use]
65pub fn map_deser(e: &serde_json::Error, body: &[u8]) -> ExaError {
66 let snippet = String::from_utf8_lossy(&body[..body.len().min(400)]).to_string();
67 ExaError::Serde(format!("{e}: {snippet}"))
68}
69
70#[must_use]
74pub fn deserialize_api_error(status: StatusCode, body: &[u8]) -> ExaError {
75 let status_code = Some(status.as_u16());
76
77 if let Ok(mut obj) = serde_json::from_slice::<ApiErrorObject>(body) {
78 obj.status_code = status_code;
79 return ExaError::Api(obj);
80 }
81
82 ExaError::Api(ApiErrorObject {
84 status_code,
85 message: String::from_utf8_lossy(&body[..body.len().min(400)]).into_owned(),
86 timestamp: None,
87 path: None,
88 error: Some(format!("http_{}", status.as_u16())),
89 })
90}