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    /// An error during LFS upload.
48    #[error("LFS upload error for {oid}: {message}")]
49    LfsUpload {
50        /// The OID of the object that failed to upload.
51        oid: String,
52        /// Human-readable error message.
53        message: String,
54    },
55}
56
57impl MesaError {
58    /// Returns `true` if this error is retryable (429 or 5xx).
59    #[must_use]
60    pub fn is_retryable(&self) -> bool {
61        match self {
62            Self::Api { status, .. } => status.as_u16() == 429 || status.is_server_error(),
63            Self::HttpClient(HttpClientError::Timeout | HttpClientError::Connection(_)) => true,
64            Self::HttpClient(HttpClientError::Other(_))
65            | Self::Serialization(_)
66            | Self::RetriesExhausted { .. }
67            | Self::LfsUpload { .. } => false,
68        }
69    }
70
71    /// Returns the HTTP status code, if this is an API error.
72    #[must_use]
73    pub fn status(&self) -> Option<StatusCode> {
74        match self {
75            Self::Api { status, .. } => Some(*status),
76            Self::HttpClient(_)
77            | Self::Serialization(_)
78            | Self::RetriesExhausted { .. }
79            | Self::LfsUpload { .. } => None,
80        }
81    }
82}
83
84/// Structured error code returned by the Mesa API.
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub enum ApiErrorCode {
87    /// 400 Bad Request.
88    BadRequest,
89    /// 401 Unauthorized.
90    Unauthorized,
91    /// 403 Forbidden.
92    Forbidden,
93    /// 404 Not Found.
94    NotFound,
95    /// 406 Not Acceptable.
96    NotAcceptable,
97    /// 409 Conflict.
98    Conflict,
99    /// 500 Internal Server Error.
100    InternalServerError,
101    /// An unrecognized error code.
102    Unknown(String),
103}
104
105impl ApiErrorCode {
106    /// Parse an error code string from the API.
107    #[must_use]
108    pub fn from_code(s: &str) -> Self {
109        match s {
110            "bad_request" => Self::BadRequest,
111            "unauthorized" => Self::Unauthorized,
112            "forbidden" => Self::Forbidden,
113            "not_found" => Self::NotFound,
114            "not_acceptable" => Self::NotAcceptable,
115            "conflict" => Self::Conflict,
116            "internal_server_error" => Self::InternalServerError,
117            other => Self::Unknown(other.to_owned()),
118        }
119    }
120}
121
122impl fmt::Display for ApiErrorCode {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        match self {
125            Self::BadRequest => f.write_str("bad_request"),
126            Self::Unauthorized => f.write_str("unauthorized"),
127            Self::Forbidden => f.write_str("forbidden"),
128            Self::NotFound => f.write_str("not_found"),
129            Self::NotAcceptable => f.write_str("not_acceptable"),
130            Self::Conflict => f.write_str("conflict"),
131            Self::InternalServerError => f.write_str("internal_server_error"),
132            Self::Unknown(code) => f.write_str(code),
133        }
134    }
135}
136
137/// Errors from the HTTP transport layer.
138///
139/// When implementing [`HttpClient`](crate::HttpClient), map your HTTP library's
140/// errors to these variants. The variant you choose determines whether the SDK
141/// retries the request:
142///
143/// | Variant | Retried? | When to use |
144/// |---------|----------|-------------|
145/// | [`Timeout`](Self::Timeout) | Yes | Request exceeded deadline |
146/// | [`Connection`](Self::Connection) | Yes | DNS, TCP, or TLS failure |
147/// | [`Other`](Self::Other) | No | Everything else |
148#[derive(Debug, thiserror::Error)]
149pub enum HttpClientError {
150    /// The request timed out.
151    #[error("Request timed out")]
152    Timeout,
153    /// A connection error occurred.
154    #[error("Connection error: {0}")]
155    Connection(String),
156    /// Any other transport error.
157    #[error("{0}")]
158    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
159}
160
161// ── Internal deserialization structs for API error responses ──
162
163/// The top-level JSON body returned on API errors.
164#[derive(Debug, Deserialize)]
165pub(crate) struct ApiErrorResponse {
166    pub error: ApiErrorBody,
167}
168
169/// The nested error object inside an API error response.
170#[derive(Debug, Deserialize)]
171pub(crate) struct ApiErrorBody {
172    pub code: String,
173    #[serde(default)]
174    pub message: String,
175    #[serde(default = "default_details")]
176    pub details: serde_json::Value,
177}
178
179fn default_details() -> serde_json::Value {
180    serde_json::Value::Null
181}