Skip to main content

canic_core/dto/
error.rs

1use crate::{access::AccessError, dto::prelude::*};
2use std::fmt::{self, Display};
3
4//
5// Error
6//
7// Public API error payload.
8//
9
10#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
11pub struct Error {
12    pub code: ErrorCode,
13    pub message: String,
14}
15
16impl Error {
17    #[must_use]
18    pub const fn new(code: ErrorCode, message: String) -> Self {
19        Self { code, message }
20    }
21
22    // 409 – Conflict with existing state or resource.
23    pub fn conflict(message: impl Into<String>) -> Self {
24        Self::new(ErrorCode::Conflict, message.into())
25    }
26
27    // 409 – Policy violation with a stable policy-specific code.
28    pub fn policy(code: ErrorCode, message: impl Into<String>) -> Self {
29        Self::new(code, message.into())
30    }
31
32    // 403 – Authenticated caller is not permitted to perform this action.
33    pub fn forbidden(message: impl Into<String>) -> Self {
34        Self::new(ErrorCode::Forbidden, message.into())
35    }
36
37    // 500 – Internal or unexpected failure.
38    pub fn internal(message: impl Into<String>) -> Self {
39        Self::new(ErrorCode::Internal, message.into())
40    }
41
42    // 400 – Invalid input or malformed request.
43    pub fn invalid(message: impl Into<String>) -> Self {
44        Self::new(ErrorCode::InvalidInput, message.into())
45    }
46
47    // 400 – Replay-sensitive command omitted its client operation ID.
48    #[must_use]
49    pub fn operation_id_required() -> Self {
50        Self::new(
51            ErrorCode::OperationIdRequired,
52            "operation_id is required for this command".to_string(),
53        )
54    }
55
56    // 500 – Broken invariant or impossible internal state.
57    pub fn invariant(message: impl Into<String>) -> Self {
58        Self::new(ErrorCode::InvariantViolation, message.into())
59    }
60
61    // 429 / 507 – Resource, quota, or capacity exhaustion.
62    pub fn exhausted(message: impl Into<String>) -> Self {
63        Self::new(ErrorCode::ResourceExhausted, message.into())
64    }
65
66    // 404 – Requested resource was not found.
67    pub fn not_found(message: impl Into<String>) -> Self {
68        Self::new(ErrorCode::NotFound, message.into())
69    }
70
71    // 401 – Caller is unauthenticated or has an invalid identity.
72    pub fn unauthorized(message: impl Into<String>) -> Self {
73        Self::new(ErrorCode::Unauthorized, message.into())
74    }
75
76    // 503 – Service is temporarily unavailable due to runtime controls.
77    pub fn unavailable(message: impl Into<String>) -> Self {
78        Self::new(ErrorCode::Unavailable, message.into())
79    }
80
81    // 401 – The delegated token itself expired and may be reminted.
82    pub fn auth_token_expired(message: impl Into<String>) -> Self {
83        Self::new(ErrorCode::AuthTokenExpired, message.into())
84    }
85
86    // 503 – Delegation proof generation is still in progress and may be retried.
87    pub fn auth_proof_pending(message: impl Into<String>) -> Self {
88        Self::new(ErrorCode::AuthProofPending, message.into())
89    }
90
91    // 503 – Role-attestation proof retrieval was not run in a direct root query context.
92    #[must_use]
93    pub fn root_data_certificate_unavailable() -> Self {
94        Self::new(
95            ErrorCode::RootDataCertificateUnavailable,
96            "root data certificate unavailable for role-attestation proof retrieval".to_string(),
97        )
98    }
99}
100
101impl Display for Error {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        write!(f, "[{:?}] {}", self.code, self.message)
104    }
105}
106
107impl From<AccessError> for Error {
108    fn from(err: AccessError) -> Self {
109        let kind = err.kind();
110        let message = err.to_string();
111        match kind {
112            crate::access::AccessErrorKind::DelegatedAuthCertExpired => {
113                Self::new(ErrorCode::AuthProofExpired, message)
114            }
115            crate::access::AccessErrorKind::DelegatedAuthTokenExpired => {
116                Self::auth_token_expired(message)
117            }
118            crate::access::AccessErrorKind::Denied => Self::unauthorized(message),
119        }
120    }
121}
122
123//
124// ErrorCode
125//
126// Stable public error codes.
127//
128
129#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
130#[non_exhaustive]
131#[remain::sorted]
132pub enum ErrorCode {
133    AuthMaterialStale,
134    AuthProofExpired,
135    AuthProofPending,
136    AuthTokenExpired,
137    Conflict,
138    Forbidden,
139    Internal,
140    InternalRpcMalformed,
141    InvalidInput,
142    InvariantViolation,
143    NotFound,
144    OperationIdRequired,
145    PolicyInstanceRequiresServiceWithDirectory,
146    PolicyReplicaRequiresServiceWithScaling,
147    PolicyRoleAlreadyRegistered,
148    PolicyShardRequiresServiceWithSharding,
149    PolicySingletonAlreadyRegisteredUnderParent,
150    ResourceExhausted,
151    RootDataCertificateUnavailable,
152    Unauthorized,
153    Unavailable,
154    WasmStoreCapacityExceeded,
155    WasmStoreChunkMissing,
156    WasmStoreHashMismatch,
157    WasmStoreManifestMissing,
158}