Skip to main content

objectstore_server/endpoints/
common.rs

1//! Common types and utilities for API endpoints.
2
3use std::error::Error;
4
5use axum::Json;
6use axum::http::StatusCode;
7use axum::response::{IntoResponse, Response};
8use http::HeaderValue;
9use objectstore_service::error::Error as ServiceError;
10use serde::{Deserialize, Serialize};
11use thiserror::Error;
12
13use crate::auth::AuthError;
14use crate::extractors::batch::BatchError;
15
16/// Error type for API operations.
17#[derive(Debug, Error)]
18pub enum ApiError {
19    /// Errors indicating malformed or illegal requests.
20    #[error("client error: {0}")]
21    Client(String),
22
23    /// Authorization/authentication errors.
24    #[error("auth error: {0}")]
25    Auth(#[from] AuthError),
26
27    /// Service errors, indicating that something went wrong when receiving or executing a request.
28    #[error("service error: {0}")]
29    Service(#[from] ServiceError),
30
31    /// Errors encountered when parsing or executing a batch request.
32    #[error("batch error: {0}")]
33    Batch(#[from] BatchError),
34
35    /// Internal server errors.
36    #[error("internal error: {0}")]
37    Internal(String),
38}
39
40/// Result type for API operations.
41pub type ApiResult<T> = Result<T, ApiError>;
42
43/// A JSON error response returned by the API.
44#[derive(Serialize, Deserialize, Debug)]
45pub struct ApiErrorResponse {
46    /// The main error message.
47    #[serde(default)]
48    detail: Option<String>,
49    /// Chain of error causes.
50    #[serde(default, skip_serializing_if = "Vec::is_empty")]
51    causes: Vec<String>,
52}
53
54impl ApiErrorResponse {
55    /// Creates an error response from an error, extracting the full cause chain.
56    pub fn from_error<E: Error + ?Sized>(error: &E) -> Self {
57        let detail = Some(error.to_string());
58
59        let mut causes = Vec::new();
60        let mut source = error.source();
61        while let Some(s) = source {
62            causes.push(s.to_string());
63            source = s.source();
64        }
65
66        Self { detail, causes }
67    }
68}
69
70impl ApiError {
71    /// Returns the HTTP status code appropriate for this error variant.
72    pub fn status(&self) -> StatusCode {
73        match &self {
74            ApiError::Client(_) => StatusCode::BAD_REQUEST,
75
76            ApiError::Batch(BatchError::BadRequest(_))
77            | ApiError::Batch(BatchError::Metadata(_))
78            | ApiError::Batch(BatchError::Multipart(_)) => StatusCode::BAD_REQUEST,
79            ApiError::Batch(BatchError::LimitExceeded(_)) => StatusCode::PAYLOAD_TOO_LARGE,
80            ApiError::Batch(BatchError::RateLimited) => StatusCode::TOO_MANY_REQUESTS,
81            ApiError::Batch(BatchError::ResponseSerialization { .. }) => {
82                StatusCode::INTERNAL_SERVER_ERROR
83            }
84
85            ApiError::Auth(AuthError::BadRequest(_)) => StatusCode::BAD_REQUEST,
86            ApiError::Auth(AuthError::ValidationFailure(_))
87            | ApiError::Auth(AuthError::VerificationFailure) => StatusCode::UNAUTHORIZED,
88            ApiError::Auth(AuthError::UnknownKey) => StatusCode::UNAUTHORIZED,
89            ApiError::Auth(AuthError::UnsupportedPresignedMethod) => StatusCode::FORBIDDEN,
90            ApiError::Auth(AuthError::NotPermitted) => StatusCode::FORBIDDEN,
91            ApiError::Auth(AuthError::InternalError(_)) => StatusCode::INTERNAL_SERVER_ERROR,
92
93            ApiError::Service(ServiceError::Client(_)) => StatusCode::BAD_REQUEST,
94            ApiError::Service(ServiceError::Metadata(_)) => StatusCode::BAD_REQUEST,
95            ApiError::Service(ServiceError::RangeNotSatisfiable { .. }) => {
96                StatusCode::RANGE_NOT_SATISFIABLE
97            }
98            ApiError::Service(ServiceError::InvalidUploadId(_)) => StatusCode::BAD_REQUEST,
99            ApiError::Service(ServiceError::AtCapacity) => StatusCode::TOO_MANY_REQUESTS,
100            ApiError::Service(ServiceError::NotImplemented) => StatusCode::NOT_IMPLEMENTED,
101            ApiError::Service(_) => StatusCode::INTERNAL_SERVER_ERROR,
102
103            ApiError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
104        }
105    }
106
107    /// Reports this error to error tracking if it indicates a server fault (5xx status).
108    ///
109    /// Call this exactly once wherever an `ApiError` is serialized into a client-visible
110    /// response: standalone responses ([`IntoResponse`]) and batch response parts.
111    pub fn capture(&self) {
112        // Captured at the source in the service layer to prevent double-logging.
113        if matches!(self, ApiError::Service(_)) {
114            return;
115        }
116
117        if self.status().is_server_error() {
118            objectstore_log::error!(!!self, "error handling request");
119        }
120    }
121}
122
123impl IntoResponse for ApiError {
124    fn into_response(self) -> Response {
125        self.capture();
126        let body = ApiErrorResponse::from_error(&self);
127        (self.status(), Json(body)).into_response()
128    }
129}
130
131/// Inserts `Accept-Ranges: bytes` into the response headers.
132pub fn insert_accept_ranges(response: &mut Response) {
133    response.headers_mut().insert(
134        http::header::ACCEPT_RANGES,
135        HeaderValue::from_static("bytes"),
136    );
137}