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]
19pub 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 pub fn is_auth_error(self) -> bool {
53 matches!(
54 self,
55 Self::Authn | Self::PermissionDenied | Self::AccessTokenNotFound
56 )
57 }
58
59 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 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 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}