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