1use http::StatusCode;
15use serde::Deserialize;
16use std::fmt;
17
18#[derive(Debug, thiserror::Error)]
20pub enum MesaError {
21 #[error("API error {status}: [{code}] {message}")]
23 Api {
24 status: StatusCode,
26 code: ApiErrorCode,
28 message: String,
30 details: serde_json::Value,
32 },
33 #[error("HTTP client error: {0}")]
35 HttpClient(#[from] HttpClientError),
36 #[error("Serialization error: {0}")]
38 Serialization(#[from] serde_json::Error),
39 #[error("Request failed after {attempts} attempts: {last_error}")]
41 RetriesExhausted {
42 attempts: u32,
44 last_error: Box<Self>,
46 },
47 #[error("LFS upload error for {oid}: {message}")]
49 LfsUpload {
50 oid: String,
52 message: String,
54 },
55}
56
57impl MesaError {
58 #[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 #[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#[derive(Debug, Clone, PartialEq, Eq)]
86pub enum ApiErrorCode {
87 BadRequest,
89 Unauthorized,
91 Forbidden,
93 NotFound,
95 NotAcceptable,
97 Conflict,
99 InternalServerError,
101 Unknown(String),
103}
104
105impl ApiErrorCode {
106 #[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#[derive(Debug, thiserror::Error)]
149pub enum HttpClientError {
150 #[error("Request timed out")]
152 Timeout,
153 #[error("Connection error: {0}")]
155 Connection(String),
156 #[error("{0}")]
158 Other(#[from] Box<dyn std::error::Error + Send + Sync>),
159}
160
161#[derive(Debug, Deserialize)]
165pub(crate) struct ApiErrorResponse {
166 pub error: ApiErrorBody,
167}
168
169#[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}