Skip to main content

mesa_dev/
error.rs

1//! Error types for the Mesa SDK.
2//!
3//! All fallible SDK methods return [`MesaError`], which covers API errors,
4//! transport failures, serialization problems, and retry exhaustion.
5//!
6//! # Retryable vs non-retryable errors
7//!
8//! The SDK automatically retries transient errors. Use [`MesaError::is_retryable`]
9//! to check retryability yourself:
10//!
11//! - **Retryable:** HTTP 429, 5xx, timeouts, connection errors
12//! - **Not retryable:** 4xx (except 429), serialization errors
13
14use http::StatusCode;
15use serde::Deserialize;
16use std::fmt;
17
18/// Top-level error type for the Mesa SDK.
19#[derive(Debug, thiserror::Error)]
20pub enum MesaError {
21    /// An error returned by the Mesa API.
22    #[error("API error {status}: [{code}] {message}")]
23    Api {
24        /// HTTP status code.
25        status: StatusCode,
26        /// Structured error code.
27        code: ApiErrorCode,
28        /// Human-readable error message.
29        message: String,
30        /// Additional error details.
31        details: serde_json::Value,
32    },
33    /// An error from the underlying HTTP client.
34    #[error("HTTP client error: {0}")]
35    HttpClient(#[from] HttpClientError),
36    /// A serialization or deserialization error.
37    #[error("Serialization error: {0}")]
38    Serialization(#[from] serde_json::Error),
39    /// All retry attempts have been exhausted.
40    #[error("Request failed after {attempts} attempts: {last_error}")]
41    RetriesExhausted {
42        /// Number of attempts made.
43        attempts: u32,
44        /// The last error encountered.
45        last_error: Box<Self>,
46    },
47}
48
49impl MesaError {
50    /// Returns `true` if this error is retryable (429 or 5xx).
51    #[must_use]
52    pub fn is_retryable(&self) -> bool {
53        match self {
54            Self::Api { status, .. } => status.as_u16() == 429 || status.is_server_error(),
55            Self::HttpClient(HttpClientError::Timeout | HttpClientError::Connection(_)) => true,
56            Self::HttpClient(HttpClientError::Other(_))
57            | Self::Serialization(_)
58            | Self::RetriesExhausted { .. } => false,
59        }
60    }
61
62    /// Returns the HTTP status code, if this is an API error.
63    #[must_use]
64    pub fn status(&self) -> Option<StatusCode> {
65        match self {
66            Self::Api { status, .. } => Some(*status),
67            Self::HttpClient(_) | Self::Serialization(_) | Self::RetriesExhausted { .. } => None,
68        }
69    }
70}
71
72/// Structured error code returned by the Mesa API.
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub enum ApiErrorCode {
75    /// 400 Bad Request.
76    BadRequest,
77    /// 401 Unauthorized.
78    Unauthorized,
79    /// 403 Forbidden.
80    Forbidden,
81    /// 404 Not Found.
82    NotFound,
83    /// 406 Not Acceptable.
84    NotAcceptable,
85    /// 409 Conflict.
86    Conflict,
87    /// 500 Internal Server Error.
88    InternalServerError,
89    /// An unrecognized error code.
90    Unknown(String),
91}
92
93impl ApiErrorCode {
94    /// Parse an error code string from the API.
95    #[must_use]
96    pub fn from_code(s: &str) -> Self {
97        match s {
98            "bad_request" => Self::BadRequest,
99            "unauthorized" => Self::Unauthorized,
100            "forbidden" => Self::Forbidden,
101            "not_found" => Self::NotFound,
102            "not_acceptable" => Self::NotAcceptable,
103            "conflict" => Self::Conflict,
104            "internal_server_error" => Self::InternalServerError,
105            other => Self::Unknown(other.to_owned()),
106        }
107    }
108}
109
110impl fmt::Display for ApiErrorCode {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        match self {
113            Self::BadRequest => f.write_str("bad_request"),
114            Self::Unauthorized => f.write_str("unauthorized"),
115            Self::Forbidden => f.write_str("forbidden"),
116            Self::NotFound => f.write_str("not_found"),
117            Self::NotAcceptable => f.write_str("not_acceptable"),
118            Self::Conflict => f.write_str("conflict"),
119            Self::InternalServerError => f.write_str("internal_server_error"),
120            Self::Unknown(code) => f.write_str(code),
121        }
122    }
123}
124
125/// Errors from the HTTP transport layer.
126///
127/// When implementing [`HttpClient`](crate::HttpClient), map your HTTP library's
128/// errors to these variants. The variant you choose determines whether the SDK
129/// retries the request:
130///
131/// | Variant | Retried? | When to use |
132/// |---------|----------|-------------|
133/// | [`Timeout`](Self::Timeout) | Yes | Request exceeded deadline |
134/// | [`Connection`](Self::Connection) | Yes | DNS, TCP, or TLS failure |
135/// | [`Other`](Self::Other) | No | Everything else |
136#[derive(Debug, thiserror::Error)]
137pub enum HttpClientError {
138    /// The request timed out.
139    #[error("Request timed out")]
140    Timeout,
141    /// A connection error occurred.
142    #[error("Connection error: {0}")]
143    Connection(String),
144    /// Any other transport error.
145    #[error("{0}")]
146    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
147}
148
149// ── Internal deserialization structs for API error responses ──
150
151/// The top-level JSON body returned on API errors.
152#[derive(Debug, Deserialize)]
153pub(crate) struct ApiErrorResponse {
154    pub error: ApiErrorBody,
155}
156
157/// The nested error object inside an API error response.
158#[derive(Debug, Deserialize)]
159pub(crate) struct ApiErrorBody {
160    pub code: String,
161    #[serde(default)]
162    pub message: String,
163    #[serde(default = "default_details")]
164    pub details: serde_json::Value,
165}
166
167fn default_details() -> serde_json::Value {
168    serde_json::Value::Null
169}