objectstore_server/endpoints/
common.rs1use std::borrow::Cow;
4use std::error::Error;
5
6use axum::Json;
7use axum::http::StatusCode;
8use axum::response::{IntoResponse, Response};
9use http::HeaderValue;
10use objectstore_service::error::{Error as ServiceError, ErrorKind as ServiceErrorKind};
11use serde::{Deserialize, Serialize};
12use thiserror::Error;
13
14use crate::auth::AuthError;
15use crate::extractors::batch::BatchError;
16use crate::rate_limits::LimitExceeded;
17
18#[derive(Serialize, Deserialize, Debug)]
20pub struct ApiErrorResponse {
21 #[serde(default)]
23 detail: Option<String>,
24 #[serde(default, skip_serializing_if = "Vec::is_empty")]
26 causes: Vec<String>,
27}
28
29impl ApiErrorResponse {
30 pub fn from_error<E: Error + ?Sized>(error: &E) -> Self {
32 let detail = Some(error.to_string());
33
34 let mut causes = Vec::new();
35 let mut source = error.source();
36 while let Some(s) = source {
37 causes.push(s.to_string());
38 source = s.source();
39 }
40
41 Self { detail, causes }
42 }
43}
44
45#[derive(Debug, Error)]
47pub enum ApiError {
48 #[error("client error: {context}")]
50 Client {
51 context: Cow<'static, str>,
53 #[source]
55 cause: Option<Box<dyn Error + Send + Sync>>,
56 },
57
58 #[error("{0}")]
60 Conflict(Cow<'static, str>),
61
62 #[error("auth error: {0}")]
64 Auth(#[from] AuthError),
65
66 #[error("service error: {0}")]
68 Service(#[from] ServiceError),
69
70 #[error("batch error: {0}")]
72 Batch(#[from] BatchError),
73
74 #[error("internal error: {context}")]
76 Internal {
77 context: Cow<'static, str>,
79 #[source]
81 cause: Option<Box<dyn Error + Send + Sync>>,
82 },
83}
84
85impl ApiError {
86 pub fn map_client<E>(context: impl Into<Cow<'static, str>>, cause: E) -> Self
88 where
89 E: Error + Send + Sync + 'static,
90 {
91 Self::Client {
92 context: context.into(),
93 cause: Some(Box::new(cause)),
94 }
95 }
96
97 pub fn client(context: impl Into<Cow<'static, str>>) -> Self {
99 Self::Client {
100 context: context.into(),
101 cause: None,
102 }
103 }
104
105 pub fn conflict(context: impl Into<Cow<'static, str>>) -> Self {
107 Self::Conflict(context.into())
108 }
109
110 pub fn internal<E>(context: impl Into<Cow<'static, str>>, cause: E) -> Self
112 where
113 E: Error + Send + Sync + 'static,
114 {
115 Self::Internal {
116 context: context.into(),
117 cause: Some(Box::new(cause)),
118 }
119 }
120
121 pub fn status(&self) -> StatusCode {
123 match &self {
124 ApiError::Client { .. } => StatusCode::BAD_REQUEST,
125 ApiError::Conflict(_) => StatusCode::CONFLICT,
126
127 ApiError::Batch(BatchError::BadRequest(_))
128 | ApiError::Batch(BatchError::Metadata(_))
129 | ApiError::Batch(BatchError::Multipart(_)) => StatusCode::BAD_REQUEST,
130 ApiError::Batch(BatchError::LimitExceeded(_)) => StatusCode::PAYLOAD_TOO_LARGE,
131 ApiError::Batch(BatchError::RateLimited(rejection)) => rejection.status(),
132 ApiError::Batch(BatchError::ResponseSerialization { .. }) => {
133 StatusCode::INTERNAL_SERVER_ERROR
134 }
135
136 ApiError::Auth(AuthError::BadRequest(_)) => StatusCode::BAD_REQUEST,
137 ApiError::Auth(AuthError::ValidationFailure(_))
138 | ApiError::Auth(AuthError::VerificationFailure) => StatusCode::UNAUTHORIZED,
139 ApiError::Auth(AuthError::UnknownKey) => StatusCode::UNAUTHORIZED,
140 ApiError::Auth(AuthError::UnsupportedPresignedMethod) => StatusCode::FORBIDDEN,
141 ApiError::Auth(AuthError::NotPermitted) => StatusCode::FORBIDDEN,
142 ApiError::Auth(AuthError::InternalError(_)) => StatusCode::INTERNAL_SERVER_ERROR,
143
144 ApiError::Service(error) => match error.kind() {
145 ServiceErrorKind::InvalidMetadata
146 | ServiceErrorKind::InvalidUploadSessionToken
147 | ServiceErrorKind::ClientStream
148 | ServiceErrorKind::ChunkExceedsUploadLength { .. }
149 | ServiceErrorKind::ChunkTooSmall { .. } => StatusCode::BAD_REQUEST,
150 ServiceErrorKind::UnknownUploadSession => StatusCode::NOT_FOUND,
151 ServiceErrorKind::RangeNotSatisfiable { .. } => StatusCode::RANGE_NOT_SATISFIABLE,
152 ServiceErrorKind::UploadOffsetMismatch { .. } => StatusCode::CONFLICT,
153 ServiceErrorKind::UploadSessionGone => StatusCode::GONE,
154 ServiceErrorKind::AtCapacity => LimitExceeded::Capacity.status(),
155 ServiceErrorKind::Unsupported => StatusCode::NOT_IMPLEMENTED,
156 ServiceErrorKind::BackendRateLimited => StatusCode::TOO_MANY_REQUESTS,
157 ServiceErrorKind::BackendTimeout | ServiceErrorKind::BackendUnavailable => {
158 StatusCode::SERVICE_UNAVAILABLE
159 }
160 ServiceErrorKind::BackendFailure
161 | ServiceErrorKind::CorruptData
162 | ServiceErrorKind::Panic
163 | ServiceErrorKind::UnexpectedTombstone
164 | ServiceErrorKind::Internal => StatusCode::INTERNAL_SERVER_ERROR,
165 },
166
167 ApiError::Internal { .. } => StatusCode::INTERNAL_SERVER_ERROR,
168 }
169 }
170
171 pub fn capture(&self) {
176 if matches!(
178 self,
179 ApiError::Service(_) | ApiError::Batch(BatchError::RateLimited(_))
180 ) {
181 return;
182 }
183
184 if self.status().is_server_error() {
185 objectstore_log::error!(!!self, "error handling request");
186 }
187 }
188}
189
190impl IntoResponse for ApiError {
191 fn into_response(self) -> Response {
192 if let ApiError::Service(e) = &self
193 && e.kind() == ServiceErrorKind::AtCapacity
194 {
195 return LimitExceeded::Capacity.into_response();
196 }
197
198 self.capture();
199 let body = ApiErrorResponse::from_error(&self);
200 (self.status(), Json(body)).into_response()
201 }
202}
203
204impl From<crate::usecases::UseCaseError> for ApiError {
205 fn from(error: crate::usecases::UseCaseError) -> Self {
206 ApiError::map_client("use case policy violation", error)
207 }
208}
209
210impl From<objectstore_types::metadata::Error> for ApiError {
211 fn from(error: objectstore_types::metadata::Error) -> Self {
212 ApiError::map_client("invalid metadata", error)
213 }
214}
215
216pub type ApiResult<T> = Result<T, ApiError>;
218
219pub fn insert_accept_ranges(response: &mut Response) {
221 response.headers_mut().insert(
222 http::header::ACCEPT_RANGES,
223 HeaderValue::from_static("bytes"),
224 );
225}