Skip to main content

exa_async/
error.rs

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