Skip to main content

sui_id_shared/
errors.rs

1//! Public-facing API error type.
2//!
3//! Internal causes are deliberately not embedded in the user-visible payload.
4//! Each error carries an opaque request id so that operators can correlate
5//! a user complaint with detailed server-side log entries without exposing
6//! internal information to the client.
7
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10
11/// Stable error code returned to API callers. The set is intentionally small
12/// so that integrators can branch on a finite alphabet rather than parsing
13/// human-readable messages.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum ApiErrorCode {
17    /// The caller is not authenticated, or credentials are invalid.
18    Unauthorized,
19    /// The caller is authenticated but lacks permission for this operation.
20    Forbidden,
21    /// The request payload failed validation.
22    BadRequest,
23    /// The requested resource does not exist.
24    NotFound,
25    /// The request conflicts with current state (e.g. duplicate resource).
26    Conflict,
27    /// The system is in setup mode and the requested endpoint is not allowed
28    /// until initialization is complete (or the endpoint requires setup mode).
29    InvalidState,
30    /// Caller is being rate-limited.
31    TooManyRequests,
32    /// An OAuth 2.0 / OIDC protocol error. The exact `error` field follows
33    /// the relevant RFCs (e.g. `invalid_grant`, `invalid_client`).
34    Protocol,
35    /// Catch-all for anything we did not anticipate. The body deliberately
36    /// does not carry internal cause; correlate via `request_id`.
37    Internal,
38}
39
40impl ApiErrorCode {
41    /// HTTP status code commonly associated with this error.
42    pub const fn http_status(self) -> u16 {
43        match self {
44            Self::Unauthorized => 401,
45            Self::Forbidden => 403,
46            Self::BadRequest | Self::Protocol => 400,
47            Self::NotFound => 404,
48            Self::Conflict => 409,
49            Self::InvalidState => 409,
50            Self::TooManyRequests => 429,
51            Self::Internal => 500,
52        }
53    }
54}
55
56/// Wire-format error payload returned by the JSON API.
57#[derive(Debug, Clone, Serialize, Deserialize, Error)]
58#[error("{code:?}: {message}")]
59pub struct ApiError {
60    pub code: ApiErrorCode,
61    pub message: String,
62    /// Opaque correlation id. Always present; clients should surface this
63    /// when reporting a problem.
64    pub request_id: String,
65    /// Optional protocol-specific subcode, e.g. OAuth `error` field.
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub protocol_code: Option<String>,
68}
69
70impl ApiError {
71    pub fn new(code: ApiErrorCode, message: impl Into<String>, request_id: impl Into<String>) -> Self {
72        Self {
73            code,
74            message: message.into(),
75            request_id: request_id.into(),
76            protocol_code: None,
77        }
78    }
79
80    pub fn with_protocol_code(mut self, sub: impl Into<String>) -> Self {
81        self.protocol_code = Some(sub.into());
82        self
83    }
84}
85
86#[cfg(test)]
87mod tests;