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;
16
17/// A JSON error response returned by the API.
18#[derive(Serialize, Deserialize, Debug)]
19pub struct ApiErrorResponse {
20    /// The main error message.
21    #[serde(default)]
22    detail: Option<String>,
23    /// Chain of error causes.
24    #[serde(default, skip_serializing_if = "Vec::is_empty")]
25    causes: Vec<String>,
26}
27
28impl ApiErrorResponse {
29    /// Creates an error response from an error, extracting the full cause chain.
30    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/// Error type for API operations.
45#[derive(Debug, Error)]
46pub enum ApiError {
47    /// Errors indicating malformed or illegal requests.
48    #[error("client error: {context}")]
49    Client {
50        /// Context describing the operation that failed.
51        context: Cow<'static, str>,
52        /// The underlying error, if available.
53        #[source]
54        cause: Option<Box<dyn Error + Send + Sync>>,
55    },
56
57    /// Authorization/authentication errors.
58    #[error("auth error: {0}")]
59    Auth(#[from] AuthError),
60
61    /// Service errors, indicating that something went wrong when receiving or executing a request.
62    #[error("service error: {0}")]
63    Service(#[from] ServiceError),
64
65    /// Errors encountered when parsing or executing a batch request.
66    #[error("batch error: {0}")]
67    Batch(#[from] BatchError),
68
69    /// Internal server errors.
70    #[error("internal error: {context}")]
71    Internal {
72        /// Context describing the operation that failed.
73        context: Cow<'static, str>,
74        /// The underlying error, if available.
75        #[source]
76        cause: Option<Box<dyn Error + Send + Sync>>,
77    },
78}
79
80impl ApiError {
81    /// Creates a client error with context and an underlying cause.
82    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    /// Creates a client error with context and no underlying cause.
93    pub fn client(context: impl Into<Cow<'static, str>>) -> Self {
94        Self::Client {
95            context: context.into(),
96            cause: None,
97        }
98    }
99
100    /// Creates an internal server error with context and an underlying cause.
101    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    /// Returns the HTTP status code appropriate for this error variant.
112    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    /// Reports this error to error tracking if it indicates a server fault (5xx status).
160    ///
161    /// Call this exactly once wherever an `ApiError` is serialized into a client-visible
162    /// response: standalone responses ([`IntoResponse`]) and batch response parts.
163    pub fn capture(&self) {
164        // Captured at the source in the service layer to prevent double-logging.
165        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
195/// Result type for API operations.
196pub type ApiResult<T> = Result<T, ApiError>;
197
198/// Inserts `Accept-Ranges: bytes` into the response headers.
199pub fn insert_accept_ranges(response: &mut Response) {
200    response.headers_mut().insert(
201        http::header::ACCEPT_RANGES,
202        HeaderValue::from_static("bytes"),
203    );
204}