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("auth error: {0}")]
59 Auth(#[from] AuthError),
60
61 #[error("service error: {0}")]
63 Service(#[from] ServiceError),
64
65 #[error("batch error: {0}")]
67 Batch(#[from] BatchError),
68
69 #[error("internal error: {context}")]
71 Internal {
72 context: Cow<'static, str>,
74 #[source]
76 cause: Option<Box<dyn Error + Send + Sync>>,
77 },
78}
79
80impl ApiError {
81 pub fn map_client<E>(context: impl Into<Cow<'static, str>>, cause: E) -> Self
83 where
84 E: Error + Send + Sync + 'static,
85 {
86 Self::Client {
87 context: context.into(),
88 cause: Some(Box::new(cause)),
89 }
90 }
91
92 pub fn client(context: impl Into<Cow<'static, str>>) -> Self {
94 Self::Client {
95 context: context.into(),
96 cause: None,
97 }
98 }
99
100 pub fn internal<E>(context: impl Into<Cow<'static, str>>, cause: E) -> Self
102 where
103 E: Error + Send + Sync + 'static,
104 {
105 Self::Internal {
106 context: context.into(),
107 cause: Some(Box::new(cause)),
108 }
109 }
110
111 pub fn status(&self) -> StatusCode {
113 match &self {
114 ApiError::Client { .. } => StatusCode::BAD_REQUEST,
115
116 ApiError::Batch(BatchError::BadRequest(_))
117 | ApiError::Batch(BatchError::Metadata(_))
118 | ApiError::Batch(BatchError::Multipart(_)) => StatusCode::BAD_REQUEST,
119 ApiError::Batch(BatchError::LimitExceeded(_)) => StatusCode::PAYLOAD_TOO_LARGE,
120 ApiError::Batch(BatchError::RateLimited) => StatusCode::TOO_MANY_REQUESTS,
121 ApiError::Batch(BatchError::ResponseSerialization { .. }) => {
122 StatusCode::INTERNAL_SERVER_ERROR
123 }
124
125 ApiError::Auth(AuthError::BadRequest(_)) => StatusCode::BAD_REQUEST,
126 ApiError::Auth(AuthError::ValidationFailure(_))
127 | ApiError::Auth(AuthError::VerificationFailure) => StatusCode::UNAUTHORIZED,
128 ApiError::Auth(AuthError::UnknownKey) => StatusCode::UNAUTHORIZED,
129 ApiError::Auth(AuthError::UnsupportedPresignedMethod) => StatusCode::FORBIDDEN,
130 ApiError::Auth(AuthError::NotPermitted) => StatusCode::FORBIDDEN,
131 ApiError::Auth(AuthError::InternalError(_)) => StatusCode::INTERNAL_SERVER_ERROR,
132
133 ApiError::Service(error) => match error.kind() {
134 ServiceErrorKind::InvalidMetadata
135 | ServiceErrorKind::InvalidUploadId
136 | ServiceErrorKind::ClientStream
137 | ServiceErrorKind::ChunkExceedsUploadLength { .. } => StatusCode::BAD_REQUEST,
138 ServiceErrorKind::UnknownUploadSession => StatusCode::NOT_FOUND,
139 ServiceErrorKind::RangeNotSatisfiable { .. } => StatusCode::RANGE_NOT_SATISFIABLE,
140 ServiceErrorKind::UploadOffsetMismatch { .. } => StatusCode::CONFLICT,
141 ServiceErrorKind::UploadSessionGone => StatusCode::GONE,
142 ServiceErrorKind::AtCapacity => StatusCode::TOO_MANY_REQUESTS,
143 ServiceErrorKind::Unsupported => StatusCode::NOT_IMPLEMENTED,
144 ServiceErrorKind::BackendRateLimited => StatusCode::TOO_MANY_REQUESTS,
145 ServiceErrorKind::BackendTimeout | ServiceErrorKind::BackendUnavailable => {
146 StatusCode::SERVICE_UNAVAILABLE
147 }
148 ServiceErrorKind::BackendFailure
149 | ServiceErrorKind::CorruptData
150 | ServiceErrorKind::Panic
151 | ServiceErrorKind::UnexpectedTombstone
152 | ServiceErrorKind::Internal => StatusCode::INTERNAL_SERVER_ERROR,
153 },
154
155 ApiError::Internal { .. } => StatusCode::INTERNAL_SERVER_ERROR,
156 }
157 }
158
159 pub fn capture(&self) {
164 if matches!(self, ApiError::Service(_)) {
166 return;
167 }
168
169 if self.status().is_server_error() {
170 objectstore_log::error!(!!self, "error handling request");
171 }
172 }
173}
174
175impl IntoResponse for ApiError {
176 fn into_response(self) -> Response {
177 self.capture();
178 let body = ApiErrorResponse::from_error(&self);
179 (self.status(), Json(body)).into_response()
180 }
181}
182
183impl From<crate::usecases::UseCaseError> for ApiError {
184 fn from(error: crate::usecases::UseCaseError) -> Self {
185 ApiError::map_client("use case policy violation", error)
186 }
187}
188
189impl From<objectstore_types::metadata::Error> for ApiError {
190 fn from(error: objectstore_types::metadata::Error) -> Self {
191 ApiError::map_client("invalid metadata", error)
192 }
193}
194
195pub type ApiResult<T> = Result<T, ApiError>;
197
198pub fn insert_accept_ranges(response: &mut Response) {
200 response.headers_mut().insert(
201 http::header::ACCEPT_RANGES,
202 HeaderValue::from_static("bytes"),
203 );
204}