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")]
18pub 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}