1use crate::types::GatewayErrorBody;
8
9#[derive(Debug, thiserror::Error)]
13#[non_exhaustive]
18pub enum SolariError {
19 #[error("{message}")]
21 Auth { status: u16, message: String, code: Option<String> },
22
23 #[error("{message}")]
26 Plan { status: u16, message: String, code: Option<String> },
27
28 #[error("{message}")]
31 ConcurrencyLimit { status: u16, message: String, code: Option<String> },
32
33 #[error("{message}")]
37 NoCapacity { status: u16, message: String, code: Option<String> },
38
39 #[error("{message}")]
41 Gateway { status: u16, message: String, code: Option<String> },
42
43 #[error("{message}")]
45 Action { method: String, message: String, code: Option<String> },
46
47 #[error("Action \"{method}\" timed out after {timeout_ms}ms")]
49 Timeout { method: String, timeout_ms: u64 },
50
51 #[error("{message}")]
53 Connection { message: String },
54
55 #[error("{message}")]
57 Git { message: String },
58
59 #[error("{0}")]
61 Other(String),
62 #[error("{message}")]
66 Validation { message: String },
67}
68
69impl SolariError {
70 pub(crate) fn connection(msg: impl Into<String>) -> Self {
71 SolariError::Connection { message: msg.into() }
72 }
73
74 pub fn status(&self) -> Option<u16> {
76 match self {
77 SolariError::Auth { status, .. }
78 | SolariError::Plan { status, .. }
79 | SolariError::ConcurrencyLimit { status, .. }
80 | SolariError::NoCapacity { status, .. }
81 | SolariError::Gateway { status, .. } => Some(*status),
82 _ => None,
83 }
84 }
85}
86
87pub(crate) fn map_gateway_error(status: u16, body: Option<GatewayErrorBody>) -> SolariError {
90 let code = body.as_ref().and_then(|b| b.code.clone());
91 let message = body
92 .as_ref()
93 .and_then(|b| b.message.clone().or_else(|| b.error.clone()).or_else(|| b.code.clone()))
94 .unwrap_or_else(|| format!("Gateway request failed with status {status}"));
95 match (status, code.as_deref()) {
98 (401, _) => SolariError::Auth { status, message, code },
99 (402, _) => SolariError::Plan { status, message, code },
100 (403, Some("NotEntitled")) => SolariError::Plan { status, message, code },
102 (429, None | Some("ConcurrencyLimitExceeded")) => SolariError::ConcurrencyLimit { status, message, code },
103 (503, None | Some("NoCapacity" | "StartTimeout")) => SolariError::NoCapacity { status, message, code },
104 _ => SolariError::Gateway { status, message, code },
105 }
106}
107
108pub(crate) fn is_route_unavailable(err: &SolariError) -> bool {
113 matches!(err.status(), Some(404) | Some(405) | Some(501))
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[derive(Debug, PartialEq)]
121 enum Kind {
122 Auth,
123 Plan,
124 Concurrency,
125 NoCapacity,
126 Plain,
127 }
128
129 fn kind(e: &SolariError) -> Kind {
130 match e {
131 SolariError::Auth { .. } => Kind::Auth,
132 SolariError::Plan { .. } => Kind::Plan,
133 SolariError::ConcurrencyLimit { .. } => Kind::Concurrency,
134 SolariError::NoCapacity { .. } => Kind::NoCapacity,
135 SolariError::Gateway { .. } => Kind::Plain,
136 other => panic!("not a gateway error: {other:?}"),
137 }
138 }
139
140 #[test]
142 fn maps_by_code_first() {
143 let cases: &[(&str, u16, Option<&str>, Kind)] = &[
144 ("401", 401, Some("Unauthorized"), Kind::Auth),
145 ("402-plan", 402, Some("FeatureRequiresPlan"), Kind::Plan),
146 ("402-credit", 402, Some("InsufficientCredit"), Kind::Plan),
147 ("403-not-entitled", 403, Some("NotEntitled"), Kind::Plan),
148 ("403-other", 403, Some("Forbidden"), Kind::Plain),
149 ("404", 404, Some("NotFound"), Kind::Plain),
150 ("409-sandbox-stopped", 409, Some("SandboxStopped"), Kind::Plain),
151 ("410-gone", 410, Some("SandboxGone"), Kind::Plain),
152 ("429-concurrency-cap", 429, Some("ConcurrencyLimitExceeded"), Kind::Concurrency),
153 ("429-no-code", 429, None, Kind::Concurrency),
154 ("429-too-many-executions", 429, Some("TooManyExecutions"), Kind::Plain),
155 ("429-object-limit", 429, Some("ObjectLimit"), Kind::Plain),
156 ("500-internal", 500, Some("InternalError"), Kind::Plain),
157 ("501-not-supported", 501, Some("NotSupported"), Kind::Plain),
158 ("503-no-capacity", 503, Some("NoCapacity"), Kind::NoCapacity),
159 ("503-start-timeout", 503, Some("StartTimeout"), Kind::NoCapacity),
160 ("503-no-code", 503, None, Kind::NoCapacity),
161 ("503-sandbox-unavailable", 503, Some("SandboxUnavailable"), Kind::Plain),
162 ("503-store-unavailable", 503, Some("StoreUnavailable"), Kind::Plain),
163 ("503-admission-unavailable", 503, Some("AdmissionUnavailable"), Kind::Plain),
164 ("503-dependency-unavailable", 503, Some("DependencyUnavailable"), Kind::Plain),
165 ("507-insufficient-capacity", 507, Some("InsufficientCapacity"), Kind::Plain),
166 ];
167 for (id, status, code, want) in cases {
168 let body = GatewayErrorBody { code: code.map(str::to_string), ..Default::default() };
169 let e = map_gateway_error(*status, Some(body));
170 assert_eq!(&kind(&e), want, "errors/{id}");
171 assert_eq!(e.status(), Some(*status), "errors/{id}: the status the gateway sent is kept");
172 let kept = match &e {
173 SolariError::Auth { code, .. }
174 | SolariError::Plan { code, .. }
175 | SolariError::ConcurrencyLimit { code, .. }
176 | SolariError::NoCapacity { code, .. }
177 | SolariError::Gateway { code, .. } => code.clone(),
178 _ => unreachable!(),
179 };
180 assert_eq!(kept.as_deref(), *code, "errors/{id}: the gateway's code is kept");
181 }
182 }
183}