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    /// The requested operation was valid but could not be satisfied.
58    #[error("{0}")]
59    Conflict(Cow<'static, str>),
60
61    /// Authorization/authentication errors.
62    #[error("auth error: {0}")]
63    Auth(#[from] AuthError),
64
65    /// Service errors, indicating that something went wrong when receiving or executing a request.
66    #[error("service error: {0}")]
67    Service(#[from] ServiceError),
68
69    /// Errors encountered when parsing or executing a batch request.
70    #[error("batch error: {0}")]
71    Batch(#[from] BatchError),
72
73    /// Internal server errors.
74    #[error("internal error: {context}")]
75    Internal {
76        /// Context describing the operation that failed.
77        context: Cow<'static, str>,
78        /// The underlying error, if available.
79        #[source]
80        cause: Option<Box<dyn Error + Send + Sync>>,
81    },
82}
83
84impl ApiError {
85    /// Creates a client error with context and an underlying cause.
86    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    /// Creates a client error with context and no underlying cause.
97    pub fn client(context: impl Into<Cow<'static, str>>) -> Self {
98        Self::Client {
99            context: context.into(),
100            cause: None,
101        }
102    }
103
104    /// Creates a conflict error with a client-safe explanation.
105    pub fn conflict(context: impl Into<Cow<'static, str>>) -> Self {
106        Self::Conflict(context.into())
107    }
108
109    /// Creates an internal server error with context and an underlying cause.
110    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    /// Returns the HTTP status code appropriate for this error variant.
121    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    /// Reports this error to error tracking if it indicates a server fault (5xx status).
171    ///
172    /// Call this exactly once wherever an `ApiError` is serialized into a client-visible
173    /// response: standalone responses ([`IntoResponse`]) and batch response parts.
174    pub fn capture(&self) {
175        // Captured at the source in the service layer to prevent double-logging.
176        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
206/// Result type for API operations.
207pub type ApiResult<T> = Result<T, ApiError>;
208
209/// Inserts `Accept-Ranges: bytes` into the response headers.
210pub fn insert_accept_ranges(response: &mut Response) {
211    response.headers_mut().insert(
212        http::header::ACCEPT_RANGES,
213        HeaderValue::from_static("bytes"),
214    );
215}