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