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// Keep this alphabetized.
19pub enum ErrorCode {
20    AccessTokenNotFound,
21    Authn,
22    BadFrame,
23    BadHeader,
24    BadJson,
25    BadPath,
26    BadProto,
27    BadQuery,
28    BasinDeletionPending,
29    BasinNotFound,
30    ClientHangup,
31    DecryptionFailed,
32    HotServer,
33    Invalid,
34    NotImplemented,
35    Other,
36    PermissionDenied,
37    QuotaExhausted,
38    RateLimited,
39    RequestTimeout,
40    ResourceAlreadyExists,
41    Storage,
42    StreamDeletionPending,
43    StreamNotFound,
44    TransactionConflict,
45    Unavailable,
46    UpstreamTimeout,
47}
48
49impl ErrorCode {
50    pub fn is_auth_error(self) -> bool {
51        matches!(
52            self,
53            Self::Authn | Self::PermissionDenied | Self::AccessTokenNotFound
54        )
55    }
56
57    pub fn status(self) -> http::StatusCode {
58        match self {
59            Self::Authn => http::StatusCode::UNAUTHORIZED,
60            Self::DecryptionFailed
61            | Self::BadFrame
62            | Self::BadHeader
63            | Self::BadJson
64            | Self::BadPath
65            | Self::BadProto
66            | Self::BadQuery => http::StatusCode::BAD_REQUEST,
67            Self::PermissionDenied | Self::QuotaExhausted => http::StatusCode::FORBIDDEN,
68            Self::AccessTokenNotFound | Self::BasinNotFound | Self::StreamNotFound => {
69                http::StatusCode::NOT_FOUND
70            }
71            Self::RequestTimeout => http::StatusCode::REQUEST_TIMEOUT,
72            Self::BasinDeletionPending
73            | Self::ResourceAlreadyExists
74            | Self::StreamDeletionPending
75            | Self::TransactionConflict => http::StatusCode::CONFLICT,
76            Self::Invalid => http::StatusCode::UNPROCESSABLE_ENTITY,
77            Self::NotImplemented => http::StatusCode::NOT_IMPLEMENTED,
78            Self::RateLimited => http::StatusCode::TOO_MANY_REQUESTS,
79            Self::ClientHangup => http::StatusCode::from_u16(499).expect("valid status code"),
80            Self::Other | Self::Storage => http::StatusCode::INTERNAL_SERVER_ERROR,
81            Self::HotServer => http::StatusCode::BAD_GATEWAY,
82            Self::Unavailable => http::StatusCode::SERVICE_UNAVAILABLE,
83            Self::UpstreamTimeout => http::StatusCode::GATEWAY_TIMEOUT,
84        }
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn auth_related_codes_are_classified() {
94        assert!(ErrorCode::Authn.is_auth_error());
95        assert!(ErrorCode::PermissionDenied.is_auth_error());
96        assert!(ErrorCode::AccessTokenNotFound.is_auth_error());
97        assert!(!ErrorCode::NotImplemented.is_auth_error());
98    }
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
102#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
103pub struct ErrorInfo {
104    pub code: &'static str,
105    pub message: String,
106}
107
108#[derive(Debug, Clone)]
109pub struct StandardError {
110    pub status: http::StatusCode,
111    pub info: ErrorInfo,
112}
113
114#[derive(Debug, Clone)]
115pub enum ErrorResponse {
116    AppendConditionFailed(super::stream::AppendConditionFailed),
117    Unwritten(super::stream::TailResponse),
118    Standard(StandardError),
119}
120
121impl ErrorResponse {
122    pub fn to_parts(&self) -> (http::StatusCode, String) {
123        let (status, res) = match self {
124            ErrorResponse::AppendConditionFailed(payload) => (
125                http::StatusCode::PRECONDITION_FAILED,
126                serde_json::to_string(&payload),
127            ),
128            ErrorResponse::Unwritten(payload) => (
129                http::StatusCode::RANGE_NOT_SATISFIABLE,
130                serde_json::to_string(&payload),
131            ),
132            ErrorResponse::Standard(err) => (err.status, serde_json::to_string(&err.info)),
133        };
134        (status, res.expect("basic json ser"))
135    }
136}
137
138#[cfg(feature = "axum")]
139impl axum::response::IntoResponse for ErrorResponse {
140    fn into_response(self) -> axum::response::Response {
141        let (status, json_str) = self.to_parts();
142        let mut response = (
143            [(
144                http::header::CONTENT_TYPE,
145                http::header::HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()),
146            )],
147            json_str,
148        )
149            .into_response();
150        *response.status_mut() = status;
151        response
152    }
153}