Skip to main content

objectstore_server/endpoints/
common.rs

1//! Common types and utilities for API endpoints.
2
3use 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/// A JSON error response returned by the API.
19#[derive(Serialize, Deserialize, Debug)]
20pub struct ApiErrorResponse {
21    /// The main error message.
22    #[serde(default)]
23    detail: Option<String>,
24    /// Chain of error causes.
25    #[serde(default, skip_serializing_if = "Vec::is_empty")]
26    causes: Vec<String>,
27}
28
29impl ApiErrorResponse {
30    /// Creates an error response from an error, extracting the full cause chain.
31    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/// Error type for API operations.
46#[derive(Debug, Error)]
47pub enum ApiError {
48    /// Errors indicating malformed or illegal requests.
49    #[error("client error: {context}")]
50    Client {
51        /// Context describing the operation that failed.
52        context: Cow<'static, str>,
53        /// The underlying error, if available.
54        #[source]
55        cause: Option<Box<dyn Error + Send + Sync>>,
56    },
57
58    /// The requested operation was valid but could not be satisfied.
59    #[error("{0}")]
60    Conflict(Cow<'static, str>),
61
62    /// Authorization/authentication errors.
63    #[error("auth error: {0}")]
64    Auth(#[from] AuthError),
65
66    /// Service errors, indicating that something went wrong when receiving or executing a request.
67    #[error("service error: {0}")]
68    Service(#[from] ServiceError),
69
70    /// Errors encountered when parsing or executing a batch request.
71    #[error("batch error: {0}")]
72    Batch(#[from] BatchError),
73
74    /// Internal server errors.
75    #[error("internal error: {context}")]
76    Internal {
77        /// Context describing the operation that failed.
78        context: Cow<'static, str>,
79        /// The underlying error, if available.
80        #[source]
81        cause: Option<Box<dyn Error + Send + Sync>>,
82    },
83}
84
85impl ApiError {
86    /// Creates a client error with context and an underlying cause.
87    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    /// Creates a client error with context and no underlying cause.
98    pub fn client(context: impl Into<Cow<'static, str>>) -> Self {
99        Self::Client {
100            context: context.into(),
101            cause: None,
102        }
103    }
104
105    /// Creates a conflict error with a client-safe explanation.
106    pub fn conflict(context: impl Into<Cow<'static, str>>) -> Self {
107        Self::Conflict(context.into())
108    }
109
110    /// Creates an internal server error with context and an underlying cause.
111    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    /// Returns the HTTP status code appropriate for this error variant.
122    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    /// Reports this error to error tracking if it indicates a server fault (5xx status).
172    ///
173    /// Call this exactly once wherever an `ApiError` is serialized into a client-visible
174    /// response: standalone responses ([`IntoResponse`]) and batch response parts.
175    pub fn capture(&self) {
176        // Service errors and admission rejections are already logged at the source.
177        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
216/// Result type for API operations.
217pub type ApiResult<T> = Result<T, ApiError>;
218
219/// Inserts `Accept-Ranges: bytes` into the response headers.
220pub fn insert_accept_ranges(response: &mut Response) {
221    response.headers_mut().insert(
222        http::header::ACCEPT_RANGES,
223        HeaderValue::from_static("bytes"),
224    );
225}