Skip to main content

multistore/
error.rs

1//! Error types for the proxy.
2
3use thiserror::Error;
4
5/// Central error type for the proxy, mapping each variant to an S3-compatible HTTP response.
6#[derive(Debug, Error)]
7pub enum ProxyError {
8    /// The requested virtual bucket does not exist in the registry.
9    #[error("bucket not found: {0}")]
10    BucketNotFound(String),
11
12    /// The requested object key was not found in the backend store.
13    #[error("no such key: {0}")]
14    NoSuchKey(String),
15
16    /// The caller's identity lacks permission for the requested operation.
17    #[error("access denied")]
18    AccessDenied,
19
20    /// The SigV4 signature in the request does not match the expected value.
21    #[error("signature mismatch")]
22    SignatureDoesNotMatch,
23
24    /// The request is malformed or contains invalid parameters.
25    #[error("invalid request: {0}")]
26    InvalidRequest(String),
27
28    /// The request contains no authentication credentials.
29    #[error("missing authentication")]
30    MissingAuth,
31
32    /// The credentials used to sign the request have expired.
33    #[error("expired credentials")]
34    ExpiredCredentials,
35
36    /// The OIDC token provided for STS role assumption is invalid or untrusted.
37    #[error("invalid OIDC token: {0}")]
38    InvalidOidcToken(String),
39
40    /// The IAM role specified in an STS request does not exist.
41    #[error("role not found: {0}")]
42    RoleNotFound(String),
43
44    /// The upstream object store backend returned an error.
45    #[error("backend error: {0}")]
46    BackendError(String),
47
48    /// A conditional request header (e.g. `If-Match`) was not satisfied.
49    #[error("precondition failed")]
50    PreconditionFailed,
51
52    /// The object has not been modified since the time specified by `If-Modified-Since`.
53    #[error("not modified")]
54    NotModified,
55
56    /// The proxy configuration is invalid or incomplete.
57    #[error("config error: {0}")]
58    ConfigError(String),
59
60    /// An unexpected internal error occurred.
61    #[error("internal error: {0}")]
62    Internal(String),
63}
64
65impl ProxyError {
66    /// Return the S3-compatible XML error code.
67    pub fn s3_error_code(&self) -> &'static str {
68        match self {
69            Self::BucketNotFound(_) => "NoSuchBucket",
70            Self::NoSuchKey(_) => "NoSuchKey",
71            Self::AccessDenied => "AccessDenied",
72            Self::SignatureDoesNotMatch => "SignatureDoesNotMatch",
73            Self::InvalidRequest(_) => "InvalidRequest",
74            Self::MissingAuth => "AccessDenied",
75            Self::ExpiredCredentials => "ExpiredToken",
76            Self::InvalidOidcToken(_) => "InvalidIdentityToken",
77            Self::RoleNotFound(_) => "AccessDenied",
78            Self::BackendError(_) => "ServiceUnavailable",
79            Self::PreconditionFailed => "PreconditionFailed",
80            Self::NotModified => "NotModified",
81            Self::ConfigError(_) => "InternalError",
82            Self::Internal(_) => "InternalError",
83        }
84    }
85
86    /// HTTP status code for this error.
87    pub fn status_code(&self) -> u16 {
88        match self {
89            Self::BucketNotFound(_) | Self::NoSuchKey(_) => 404,
90            Self::AccessDenied | Self::MissingAuth | Self::ExpiredCredentials => 403,
91            Self::SignatureDoesNotMatch => 403,
92            Self::InvalidRequest(_) => 400,
93            Self::InvalidOidcToken(_) => 400,
94            Self::RoleNotFound(_) => 403,
95            Self::PreconditionFailed => 412,
96            Self::NotModified => 304,
97            Self::BackendError(_) => 503,
98            Self::ConfigError(_) | Self::Internal(_) => 500,
99        }
100    }
101
102    /// Return a message safe to show to external clients.
103    ///
104    /// For server-side errors (5xx), returns a generic message to avoid
105    /// leaking backend infrastructure details. For client errors (4xx),
106    /// returns the full message (the client already knows the bucket name,
107    /// key, etc.).
108    pub fn safe_message(&self) -> String {
109        match self {
110            Self::BackendError(_) => "Service unavailable".to_string(),
111            Self::ConfigError(_) | Self::Internal(_) => "Internal server error".to_string(),
112            other => other.to_string(),
113        }
114    }
115
116    /// Convert an `object_store::Error` into a `ProxyError`.
117    pub fn from_object_store_error(e: object_store::Error) -> Self {
118        match e {
119            object_store::Error::NotFound { path, .. } => Self::NoSuchKey(path),
120            object_store::Error::Precondition { .. } => Self::PreconditionFailed,
121            object_store::Error::NotModified { .. } => Self::NotModified,
122            _ => Self::BackendError(e.to_string()),
123        }
124    }
125}