1use serde::Deserialize;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum SolariErrorCode {
16 FeatureRequiresPlan,
18 ConcurrencyLimitExceeded,
20 PlanLimitExceeded,
22 BrowserUnhealthy,
24 InvalidSessionId,
28 ProxyTierUnavailable,
31 Other(String),
33}
34
35impl SolariErrorCode {
36 pub const FEATURE_REQUIRES_PLAN: &'static str = "FeatureRequiresPlan";
37 pub const CONCURRENCY_LIMIT_EXCEEDED: &'static str = "ConcurrencyLimitExceeded";
38 pub const PLAN_LIMIT_EXCEEDED: &'static str = "PlanLimitExceeded";
39 pub const BROWSER_UNHEALTHY: &'static str = "BrowserUnhealthy";
40 pub const INVALID_SESSION_ID: &'static str = "InvalidSessionId";
41 pub const PROXY_TIER_UNAVAILABLE: &'static str = "ProxyTierUnavailable";
42
43 pub fn as_str(&self) -> &str {
45 match self {
46 SolariErrorCode::FeatureRequiresPlan => Self::FEATURE_REQUIRES_PLAN,
47 SolariErrorCode::ConcurrencyLimitExceeded => Self::CONCURRENCY_LIMIT_EXCEEDED,
48 SolariErrorCode::PlanLimitExceeded => Self::PLAN_LIMIT_EXCEEDED,
49 SolariErrorCode::BrowserUnhealthy => Self::BROWSER_UNHEALTHY,
50 SolariErrorCode::InvalidSessionId => Self::INVALID_SESSION_ID,
51 SolariErrorCode::ProxyTierUnavailable => Self::PROXY_TIER_UNAVAILABLE,
52 SolariErrorCode::Other(s) => s,
53 }
54 }
55}
56
57impl From<&str> for SolariErrorCode {
58 fn from(s: &str) -> Self {
59 match s {
60 Self::FEATURE_REQUIRES_PLAN => SolariErrorCode::FeatureRequiresPlan,
61 Self::CONCURRENCY_LIMIT_EXCEEDED => SolariErrorCode::ConcurrencyLimitExceeded,
62 Self::PLAN_LIMIT_EXCEEDED => SolariErrorCode::PlanLimitExceeded,
63 Self::BROWSER_UNHEALTHY => SolariErrorCode::BrowserUnhealthy,
64 Self::INVALID_SESSION_ID => SolariErrorCode::InvalidSessionId,
65 Self::PROXY_TIER_UNAVAILABLE => SolariErrorCode::ProxyTierUnavailable,
66 other => SolariErrorCode::Other(other.to_string()),
67 }
68 }
69}
70
71impl From<String> for SolariErrorCode {
72 fn from(s: String) -> Self {
73 SolariErrorCode::from(s.as_str())
74 }
75}
76
77impl std::fmt::Display for SolariErrorCode {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 f.write_str(self.as_str())
80 }
81}
82
83#[derive(Debug, thiserror::Error)]
85pub enum SolariError {
86 #[error("{message}")]
88 Api {
89 status: u16,
90 code: Option<SolariErrorCode>,
91 message: String,
92 },
93
94 #[error("{message}")]
96 Transport { message: String },
97
98 #[error("{message}")]
100 Protocol { message: String },
101
102 #[error("{message}")]
104 Config { message: String },
105
106 #[error("{message}")]
108 Connect { message: String },
109}
110
111impl SolariError {
112 pub(crate) fn transport(msg: impl Into<String>) -> Self {
113 SolariError::Transport { message: msg.into() }
114 }
115
116 pub(crate) fn protocol(msg: impl Into<String>) -> Self {
117 SolariError::Protocol { message: msg.into() }
118 }
119
120 pub(crate) fn config(msg: impl Into<String>) -> Self {
121 SolariError::Config { message: msg.into() }
122 }
123
124 pub fn status(&self) -> Option<u16> {
126 match self {
127 SolariError::Api { status, .. } => Some(*status),
128 _ => None,
129 }
130 }
131
132 pub fn code(&self) -> Option<&SolariErrorCode> {
134 match self {
135 SolariError::Api { code, .. } => code.as_ref(),
136 _ => None,
137 }
138 }
139}
140
141#[derive(Debug, Clone, Default, Deserialize)]
144pub struct ApiErrorBody {
145 pub code: Option<String>,
146 pub error: Option<String>,
147 pub message: Option<String>,
148}
149
150pub(crate) fn api_error(method: &str, path: &str, status: u16, body: &str) -> SolariError {
154 let parsed: Option<ApiErrorBody> = serde_json::from_str(body).ok();
155 let code = parsed
156 .as_ref()
157 .and_then(|b| b.code.as_deref())
158 .map(SolariErrorCode::from);
159 SolariError::Api {
160 status,
161 code,
162 message: format!(
163 "Solari {method} {} failed: {status} {body}",
164 crate::http::redact_path(path)
165 ),
166 }
167}
168
169#[cfg(test)]
170mod redaction_tests {
171 use super::api_error;
172
173 #[test]
174 fn an_api_error_does_not_print_the_session_id() {
175 let e = api_error("GET", "/sessions/pool:abc:org:123.IDSECRET", 404, "{}").to_string();
176 assert!(!e.contains("IDSECRET"), "{e}");
177 assert!(e.contains("/sessions/pool:abc…"), "{e}");
178 }
179}