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