Skip to main content

exa_async/
error.rs

1use reqwest::StatusCode;
2use serde::Deserialize;
3use serde::Serialize;
4use thiserror::Error;
5
6/// Errors that can occur when using the Exa API client
7#[derive(Debug, Error)]
8pub enum ExaError {
9    /// HTTP request error
10    #[error("HTTP error: {0}")]
11    Reqwest(#[from] reqwest::Error),
12
13    /// API error returned by Exa
14    #[error("API error: {0:?}")]
15    Api(ApiErrorObject),
16
17    /// Configuration error (e.g., missing credentials)
18    #[error("Invalid configuration: {0}")]
19    Config(String),
20
21    /// Serialization/deserialization error
22    #[error("Serialization error: {0}")]
23    Serde(String),
24}
25
26/// API error object from Exa
27#[derive(Debug, Clone, Serialize, Deserialize)]
28#[serde(rename_all = "camelCase")]
29pub struct ApiErrorObject {
30    /// HTTP status code
31    #[serde(default)]
32    pub status_code: Option<u16>,
33    /// Human-readable error message
34    #[serde(default)]
35    pub message: String,
36    /// Timestamp of the error
37    #[serde(default)]
38    pub timestamp: Option<String>,
39    /// Request path
40    #[serde(default)]
41    pub path: Option<String>,
42    /// Error type string
43    #[serde(default)]
44    pub error: Option<String>,
45}
46
47impl ExaError {
48    /// Determines if this error is retryable
49    ///
50    /// Retryable errors include rate limits (429), timeouts (408),
51    /// and server errors (5xx).
52    #[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/// Maps a serde deserialization error to an `ExaError` with context
65#[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/// Deserializes an API error from the response body
72///
73/// Attempts to parse the error as JSON, falling back to plain text on failure.
74#[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    // Server may return plain text on 5xx; cap body to avoid log/memory bloat
84    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}