Skip to main content

systemprompt_models/api/errors/
mod.rs

1//! The single HTTP error model: [`ApiError`] with its [`ErrorCode`] status
2//! class, plus [`ValidationError`] field detail.
3//!
4//! An [`ApiError`] carries a stable machine code, a public message and,
5//! optionally, the internal cause as a source that is logged and never
6//! serialised. The wire shape enforces the redaction rule itself: a 5xx
7//! serialises the fixed public message of its code with no details or
8//! validation errors, whatever text the error was built with, so internal
9//! error text cannot reach a response body. Repository errors convert through
10//! the one canonical `From<RepositoryError>` mapping in the `repository`
11//! module; an identifier that fails to parse converts through
12//! `From<IdValidationError>` (400) in the `identifier` module.
13//!
14//! Copyright (c) systemprompt.io — Business Source License 1.1.
15//! See <https://systemprompt.io> for licensing details.
16
17mod config_plane;
18mod extension;
19mod identifier;
20mod repository;
21mod status;
22mod wire;
23
24use std::error::Error;
25
26use chrono::{DateTime, Utc};
27use serde::{Deserialize, Serialize};
28use serde_json::Value;
29use systemprompt_identifiers::TraceId;
30use systemprompt_traits::BoxedSource;
31
32#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(rename_all = "snake_case")]
34pub enum ErrorCode {
35    NotFound,
36    BadRequest,
37    Unauthorized,
38    Forbidden,
39    InternalError,
40    ValidationError,
41    ConflictError,
42    PayloadTooLarge,
43    Locked,
44    RateLimited,
45    Gone,
46    ServiceUnavailable,
47}
48
49impl ErrorCode {
50    #[must_use]
51    pub const fn is_server_error(self) -> bool {
52        matches!(self, Self::InternalError | Self::ServiceUnavailable)
53    }
54
55    #[must_use]
56    pub const fn public_server_message(self) -> &'static str {
57        match self {
58            Self::ServiceUnavailable => "Service temporarily unavailable",
59            _ => "Internal server error",
60        }
61    }
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct ValidationError {
66    pub field: String,
67    pub message: String,
68    pub code: String,
69    #[serde(skip_serializing_if = "Option::is_none")]
70    // JSON: Validation context echoes the offending request fragment, whatever its shape.
71    pub context: Option<Value>,
72}
73
74/// The HTTP error envelope every non-protocol route answers with.
75#[derive(Debug, Deserialize)]
76pub struct ApiError {
77    pub code: ErrorCode,
78    pub message: String,
79    #[serde(default)]
80    pub details: Option<String>,
81    #[serde(default)]
82    pub error_key: Option<String>,
83    #[serde(default)]
84    pub path: Option<String>,
85    #[serde(default)]
86    pub validation_errors: Vec<ValidationError>,
87    pub timestamp: DateTime<Utc>,
88    #[serde(default)]
89    pub trace_id: Option<TraceId>,
90    #[serde(skip)]
91    source: Option<BoxedSource>,
92}
93
94impl ApiError {
95    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
96        Self {
97            code,
98            message: message.into(),
99            details: None,
100            error_key: None,
101            path: None,
102            validation_errors: Vec::new(),
103            timestamp: Utc::now(),
104            trace_id: None,
105            source: None,
106        }
107    }
108
109    #[must_use]
110    pub fn with_details(mut self, details: impl Into<String>) -> Self {
111        self.details = Some(details.into());
112        self
113    }
114
115    #[must_use]
116    pub fn with_error_key(mut self, key: impl Into<String>) -> Self {
117        self.error_key = Some(key.into());
118        self
119    }
120
121    #[must_use]
122    pub fn with_path(mut self, path: impl Into<String>) -> Self {
123        self.path = Some(path.into());
124        self
125    }
126
127    #[must_use]
128    pub fn with_validation_errors(mut self, errors: Vec<ValidationError>) -> Self {
129        self.validation_errors = errors;
130        self
131    }
132
133    #[must_use]
134    pub fn with_trace_id(mut self, id: TraceId) -> Self {
135        self.trace_id = Some(id);
136        self
137    }
138
139    #[must_use]
140    pub fn with_source(mut self, source: impl Into<BoxedSource>) -> Self {
141        self.source = Some(source.into());
142        self
143    }
144
145    pub fn source(&self) -> Option<&(dyn Error + Send + Sync + 'static)> {
146        self.source.as_deref()
147    }
148
149    pub fn not_found(message: impl Into<String>) -> Self {
150        Self::new(ErrorCode::NotFound, message)
151    }
152
153    pub fn bad_request(message: impl Into<String>) -> Self {
154        Self::new(ErrorCode::BadRequest, message)
155    }
156
157    pub fn unauthorized(message: impl Into<String>) -> Self {
158        Self::new(ErrorCode::Unauthorized, message)
159    }
160
161    pub fn forbidden(message: impl Into<String>) -> Self {
162        Self::new(ErrorCode::Forbidden, message)
163    }
164
165    pub fn conflict(message: impl Into<String>) -> Self {
166        Self::new(ErrorCode::ConflictError, message)
167    }
168
169    pub fn payload_too_large(message: impl Into<String>) -> Self {
170        Self::new(ErrorCode::PayloadTooLarge, message)
171    }
172
173    pub fn locked(message: impl Into<String>) -> Self {
174        Self::new(ErrorCode::Locked, message)
175    }
176
177    pub fn rate_limited(message: impl Into<String>) -> Self {
178        Self::new(ErrorCode::RateLimited, message)
179    }
180
181    pub fn gone(message: impl Into<String>) -> Self {
182        Self::new(ErrorCode::Gone, message)
183    }
184
185    pub fn validation_error(message: impl Into<String>, errors: Vec<ValidationError>) -> Self {
186        Self::new(ErrorCode::ValidationError, message).with_validation_errors(errors)
187    }
188
189    pub fn internal(context: &'static str, source: impl Into<BoxedSource>) -> Self {
190        Self::new(ErrorCode::InternalError, context).with_source(source)
191    }
192
193    pub fn service_unavailable(context: &'static str) -> Self {
194        Self::new(ErrorCode::ServiceUnavailable, context)
195    }
196}