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 objectstore_service::error::Error as ServiceError;
9use serde::{Deserialize, Serialize};
10use thiserror::Error;
11
12use crate::auth::AuthError;
13use crate::extractors::batch::BatchError;
14
15/// Error type for API operations.
16#[derive(Debug, Error)]
17pub enum ApiError {
18    /// Errors indicating malformed or illegal requests.
19    #[error("client error: {0}")]
20    Client(String),
21
22    /// Authorization/authentication errors.
23    #[error("auth error: {0}")]
24    Auth(#[from] AuthError),
25
26    /// Service errors, indicating that something went wrong when receiving or executing a request.
27    #[error("service error: {0}")]
28    Service(#[from] ServiceError),
29
30    /// Errors encountered when parsing or executing a batch request.
31    #[error("batch error: {0}")]
32    Batch(#[from] BatchError),
33}
34
35/// Result type for API operations.
36pub type ApiResult<T> = Result<T, ApiError>;
37
38/// A JSON error response returned by the API.
39#[derive(Serialize, Deserialize, Debug)]
40pub struct ApiErrorResponse {
41    /// The main error message.
42    #[serde(default)]
43    detail: Option<String>,
44    /// Chain of error causes.
45    #[serde(default, skip_serializing_if = "Vec::is_empty")]
46    causes: Vec<String>,
47}
48
49impl ApiErrorResponse {
50    /// Creates an error response from an error, extracting the full cause chain.
51    pub fn from_error<E: Error + ?Sized>(error: &E) -> Self {
52        let detail = Some(error.to_string());
53
54        let mut causes = Vec::new();
55        let mut source = error.source();
56        while let Some(s) = source {
57            causes.push(s.to_string());
58            source = s.source();
59        }
60
61        Self { detail, causes }
62    }
63}
64
65impl ApiError {
66    /// Returns the HTTP status code appropriate for this error variant.
67    pub fn status(&self) -> StatusCode {
68        match &self {
69            ApiError::Client(_) => StatusCode::BAD_REQUEST,
70
71            ApiError::Batch(BatchError::BadRequest(_))
72            | ApiError::Batch(BatchError::Metadata(_))
73            | ApiError::Batch(BatchError::Multipart(_)) => StatusCode::BAD_REQUEST,
74            ApiError::Batch(BatchError::LimitExceeded(_)) => StatusCode::PAYLOAD_TOO_LARGE,
75            ApiError::Batch(BatchError::RateLimited) => StatusCode::TOO_MANY_REQUESTS,
76            ApiError::Batch(BatchError::ResponseSerialization { .. }) => {
77                tracing::error!(
78                    error = self as &dyn Error,
79                    "error serializing batch response"
80                );
81                StatusCode::INTERNAL_SERVER_ERROR
82            }
83
84            ApiError::Auth(AuthError::BadRequest(_)) => StatusCode::BAD_REQUEST,
85            ApiError::Auth(AuthError::ValidationFailure(_))
86            | ApiError::Auth(AuthError::VerificationFailure) => StatusCode::UNAUTHORIZED,
87            ApiError::Auth(AuthError::NotPermitted) => StatusCode::FORBIDDEN,
88            ApiError::Auth(AuthError::InternalError(_)) => {
89                tracing::error!(error = self as &dyn Error, "auth system error");
90                StatusCode::INTERNAL_SERVER_ERROR
91            }
92
93            ApiError::Service(ServiceError::Metadata(_)) => StatusCode::BAD_REQUEST,
94            ApiError::Service(ServiceError::AtCapacity) => StatusCode::TOO_MANY_REQUESTS,
95            ApiError::Service(_) => {
96                tracing::error!(error = self as &dyn Error, "error handling request");
97                StatusCode::INTERNAL_SERVER_ERROR
98            }
99        }
100    }
101}
102
103impl IntoResponse for ApiError {
104    fn into_response(self) -> Response {
105        let body = ApiErrorResponse::from_error(&self);
106        (self.status(), Json(body)).into_response()
107    }
108}