Skip to main content

s2_api/v1/
error.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(
4    Debug,
5    Clone,
6    Copy,
7    PartialEq,
8    Eq,
9    Hash,
10    Serialize,
11    Deserialize,
12    strum::Display,
13    strum::EnumString,
14    strum::IntoStaticStr,
15)]
16#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
17#[strum(serialize_all = "snake_case")]
18#[non_exhaustive]
19// Keep this alphabetized.
20pub enum ErrorCode {
21    AccessTokenNotFound,
22    Authn,
23    BadFrame,
24    BadHeader,
25    BadJson,
26    BadPath,
27    BadProto,
28    BadQuery,
29    BasinDeletionPending,
30    BasinNotFound,
31    ClientHangup,
32    DecryptionFailed,
33    HotServer,
34    Invalid,
35    NotImplemented,
36    Other,
37    PermissionDenied,
38    QuotaExhausted,
39    RateLimited,
40    RequestTimeout,
41    ResourceAlreadyExists,
42    ServerDraining,
43    Storage,
44    StreamDeletionPending,
45    StreamNotFound,
46    TransactionConflict,
47    Unavailable,
48    UpstreamTimeout,
49}
50
51impl ErrorCode {
52    /// Whether this code represents an authentication or authorization failure.
53    pub fn is_auth_error(self) -> bool {
54        matches!(
55            self,
56            Self::Authn | Self::PermissionDenied | Self::AccessTokenNotFound
57        )
58    }
59
60    /// HTTP status associated with this error code.
61    pub fn status(self) -> http::StatusCode {
62        match self {
63            Self::Authn => http::StatusCode::UNAUTHORIZED,
64            Self::DecryptionFailed
65            | Self::BadFrame
66            | Self::BadHeader
67            | Self::BadJson
68            | Self::BadPath
69            | Self::BadProto
70            | Self::BadQuery => http::StatusCode::BAD_REQUEST,
71            Self::PermissionDenied | Self::QuotaExhausted => http::StatusCode::FORBIDDEN,
72            Self::AccessTokenNotFound | Self::BasinNotFound | Self::StreamNotFound => {
73                http::StatusCode::NOT_FOUND
74            }
75            Self::RequestTimeout => http::StatusCode::REQUEST_TIMEOUT,
76            Self::BasinDeletionPending
77            | Self::ResourceAlreadyExists
78            | Self::StreamDeletionPending
79            | Self::TransactionConflict => http::StatusCode::CONFLICT,
80            Self::Invalid => http::StatusCode::UNPROCESSABLE_ENTITY,
81            Self::NotImplemented => http::StatusCode::NOT_IMPLEMENTED,
82            Self::RateLimited => http::StatusCode::TOO_MANY_REQUESTS,
83            Self::ClientHangup => http::StatusCode::from_u16(499).expect("valid status code"),
84            Self::Other | Self::Storage => http::StatusCode::INTERNAL_SERVER_ERROR,
85            Self::HotServer => http::StatusCode::BAD_GATEWAY,
86            Self::ServerDraining | Self::Unavailable => http::StatusCode::SERVICE_UNAVAILABLE,
87            Self::UpstreamTimeout => http::StatusCode::GATEWAY_TIMEOUT,
88        }
89    }
90
91    /// Whether retrying an operation that returned this code is sensible.
92    pub fn is_retryable(self) -> bool {
93        match self {
94            Self::RequestTimeout
95            | Self::TransactionConflict
96            | Self::RateLimited
97            | Self::Other
98            | Self::ServerDraining
99            | Self::Storage
100            | Self::HotServer
101            | Self::Unavailable
102            | Self::UpstreamTimeout => true,
103            Self::AccessTokenNotFound
104            | Self::Authn
105            | Self::BadFrame
106            | Self::BadHeader
107            | Self::BadJson
108            | Self::BadPath
109            | Self::BadProto
110            | Self::BadQuery
111            | Self::BasinDeletionPending
112            | Self::BasinNotFound
113            | Self::ClientHangup
114            | Self::DecryptionFailed
115            | Self::Invalid
116            | Self::NotImplemented
117            | Self::PermissionDenied
118            | Self::QuotaExhausted
119            | Self::ResourceAlreadyExists
120            | Self::StreamDeletionPending
121            | Self::StreamNotFound => false,
122        }
123    }
124
125    /// Whether the server guarantees that an operation returning this code had no side effects.
126    pub fn has_no_side_effects(self) -> bool {
127        match self {
128            Self::AccessTokenNotFound
129            | Self::Authn
130            | Self::BadFrame
131            | Self::BadHeader
132            | Self::BadJson
133            | Self::BadPath
134            | Self::BadProto
135            | Self::BadQuery
136            | Self::BasinDeletionPending
137            | Self::BasinNotFound
138            | Self::DecryptionFailed
139            | Self::HotServer
140            | Self::Invalid
141            | Self::NotImplemented
142            | Self::PermissionDenied
143            | Self::QuotaExhausted
144            | Self::RateLimited
145            | Self::ResourceAlreadyExists
146            | Self::ServerDraining
147            | Self::StreamDeletionPending
148            | Self::StreamNotFound
149            | Self::TransactionConflict => true,
150            Self::ClientHangup
151            | Self::Other
152            | Self::RequestTimeout
153            | Self::Storage
154            | Self::Unavailable
155            | Self::UpstreamTimeout => false,
156        }
157    }
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
161#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
162pub struct ErrorInfo {
163    pub code: &'static str,
164    pub message: String,
165}
166
167#[derive(Debug, Clone)]
168pub struct StandardError {
169    pub status: http::StatusCode,
170    pub info: ErrorInfo,
171}
172
173#[derive(Debug, Clone)]
174pub enum ErrorResponse {
175    AppendConditionFailed(super::stream::AppendConditionFailed),
176    Unwritten(super::stream::TailResponse),
177    Standard(StandardError),
178}
179
180impl ErrorResponse {
181    pub fn to_parts(&self) -> (http::StatusCode, String) {
182        let (status, res) = match self {
183            ErrorResponse::AppendConditionFailed(payload) => (
184                http::StatusCode::PRECONDITION_FAILED,
185                serde_json::to_string(&payload),
186            ),
187            ErrorResponse::Unwritten(payload) => (
188                http::StatusCode::RANGE_NOT_SATISFIABLE,
189                serde_json::to_string(&payload),
190            ),
191            ErrorResponse::Standard(err) => (err.status, serde_json::to_string(&err.info)),
192        };
193        (status, res.expect("basic json ser"))
194    }
195}
196
197#[cfg(feature = "axum")]
198impl axum::response::IntoResponse for ErrorResponse {
199    fn into_response(self) -> axum::response::Response {
200        let (status, json_str) = self.to_parts();
201        let mut response = (
202            [(
203                http::header::CONTENT_TYPE,
204                http::header::HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()),
205            )],
206            json_str,
207        )
208            .into_response();
209        *response.status_mut() = status;
210        response
211    }
212}