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;
16
17#[derive(Serialize, Deserialize, Debug)]
19pub struct ApiErrorResponse {
20 #[serde(default)]
22 detail: Option<String>,
23 #[serde(default, skip_serializing_if = "Vec::is_empty")]
25 causes: Vec<String>,
26}
27
28impl ApiErrorResponse {
29 pub fn from_error<E: Error + ?Sized>(error: &E) -> Self {
31 let detail = Some(error.to_string());
32
33 let mut causes = Vec::new();
34 let mut source = error.source();
35 while let Some(s) = source {
36 causes.push(s.to_string());
37 source = s.source();
38 }
39
40 Self { detail, causes }
41 }
42}
43
44#[derive(Debug, Error)]
46pub enum ApiError {
47 #[error("client error: {context}")]
49 Client {
50 context: Cow<'static, str>,
52 #[source]
54 cause: Option<Box<dyn Error + Send + Sync>>,
55 },
56
57 #[error("{0}")]
59 Conflict(Cow<'static, str>),
60
61 #[error("auth error: {0}")]
63 Auth(#[from] AuthError),
64
65 #[error("service error: {0}")]
67 Service(#[from] ServiceError),
68
69 #[error("batch error: {0}")]
71 Batch(#[from] BatchError),
72
73 #[error("internal error: {context}")]
75 Internal {
76 context: Cow<'static, str>,
78 #[source]
80 cause: Option<Box<dyn Error + Send + Sync>>,
81 },
82}
83
84impl ApiError {
85 pub fn map_client<E>(context: impl Into<Cow<'static, str>>, cause: E) -> Self
87 where
88 E: Error + Send + Sync + 'static,
89 {
90 Self::Client {
91 context: context.into(),
92 cause: Some(Box::new(cause)),
93 }
94 }
95
96 pub fn client(context: impl Into<Cow<'static, str>>) -> Self {
98 Self::Client {
99 context: context.into(),
100 cause: None,
101 }
102 }
103
104 pub fn conflict(context: impl Into<Cow<'static, str>>) -> Self {
106 Self::Conflict(context.into())
107 }
108
109 pub fn internal<E>(context: impl Into<Cow<'static, str>>, cause: E) -> Self
111 where
112 E: Error + Send + Sync + 'static,
113 {
114 Self::Internal {
115 context: context.into(),
116 cause: Some(Box::new(cause)),
117 }
118 }
119
120 pub fn status(&self) -> StatusCode {
122 match &self {
123 ApiError::Client { .. } => StatusCode::BAD_REQUEST,
124 ApiError::Conflict(_) => StatusCode::CONFLICT,
125
126 ApiError::Batch(BatchError::BadRequest(_))
127 | ApiError::Batch(BatchError::Metadata(_))
128 | ApiError::Batch(BatchError::Multipart(_)) => StatusCode::BAD_REQUEST,
129 ApiError::Batch(BatchError::LimitExceeded(_)) => StatusCode::PAYLOAD_TOO_LARGE,
130 ApiError::Batch(BatchError::RateLimited) => StatusCode::TOO_MANY_REQUESTS,
131 ApiError::Batch(BatchError::ResponseSerialization { .. }) => {
132 StatusCode::INTERNAL_SERVER_ERROR
133 }
134
135 ApiError::Auth(AuthError::BadRequest(_)) => StatusCode::BAD_REQUEST,
136 ApiError::Auth(AuthError::ValidationFailure(_))
137 | ApiError::Auth(AuthError::VerificationFailure) => StatusCode::UNAUTHORIZED,
138 ApiError::Auth(AuthError::UnknownKey) => StatusCode::UNAUTHORIZED,
139 ApiError::Auth(AuthError::UnsupportedPresignedMethod) => StatusCode::FORBIDDEN,
140 ApiError::Auth(AuthError::NotPermitted) => StatusCode::FORBIDDEN,
141 ApiError::Auth(AuthError::InternalError(_)) => StatusCode::INTERNAL_SERVER_ERROR,
142
143 ApiError::Service(error) => match error.kind() {
144 ServiceErrorKind::InvalidMetadata
145 | ServiceErrorKind::InvalidUploadId
146 | ServiceErrorKind::ClientStream
147 | ServiceErrorKind::ChunkExceedsUploadLength { .. }
148 | ServiceErrorKind::ChunkTooSmall { .. } => StatusCode::BAD_REQUEST,
149 ServiceErrorKind::UnknownUploadSession => StatusCode::NOT_FOUND,
150 ServiceErrorKind::RangeNotSatisfiable { .. } => StatusCode::RANGE_NOT_SATISFIABLE,
151 ServiceErrorKind::UploadOffsetMismatch { .. } => StatusCode::CONFLICT,
152 ServiceErrorKind::UploadSessionGone => StatusCode::GONE,
153 ServiceErrorKind::AtCapacity => StatusCode::TOO_MANY_REQUESTS,
154 ServiceErrorKind::Unsupported => StatusCode::NOT_IMPLEMENTED,
155 ServiceErrorKind::BackendRateLimited => StatusCode::TOO_MANY_REQUESTS,
156 ServiceErrorKind::BackendTimeout | ServiceErrorKind::BackendUnavailable => {
157 StatusCode::SERVICE_UNAVAILABLE
158 }
159 ServiceErrorKind::BackendFailure
160 | ServiceErrorKind::CorruptData
161 | ServiceErrorKind::Panic
162 | ServiceErrorKind::UnexpectedTombstone
163 | ServiceErrorKind::Internal => StatusCode::INTERNAL_SERVER_ERROR,
164 },
165
166 ApiError::Internal { .. } => StatusCode::INTERNAL_SERVER_ERROR,
167 }
168 }
169
170 pub fn capture(&self) {
175 if matches!(self, ApiError::Service(_)) {
177 return;
178 }
179
180 if self.status().is_server_error() {
181 objectstore_log::error!(!!self, "error handling request");
182 }
183 }
184}
185
186impl IntoResponse for ApiError {
187 fn into_response(self) -> Response {
188 self.capture();
189 let body = ApiErrorResponse::from_error(&self);
190 (self.status(), Json(body)).into_response()
191 }
192}
193
194impl From<crate::usecases::UseCaseError> for ApiError {
195 fn from(error: crate::usecases::UseCaseError) -> Self {
196 ApiError::map_client("use case policy violation", error)
197 }
198}
199
200impl From<objectstore_types::metadata::Error> for ApiError {
201 fn from(error: objectstore_types::metadata::Error) -> Self {
202 ApiError::map_client("invalid metadata", error)
203 }
204}
205
206pub type ApiResult<T> = Result<T, ApiError>;
208
209pub fn insert_accept_ranges(response: &mut Response) {
211 response.headers_mut().insert(
212 http::header::ACCEPT_RANGES,
213 HeaderValue::from_static("bytes"),
214 );
215}