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