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    ServiceUnavailable,
46}
47
48impl ErrorCode {
49    #[must_use]
50    pub const fn is_server_error(self) -> bool {
51        matches!(self, Self::InternalError | Self::ServiceUnavailable)
52    }
53
54    #[must_use]
55    pub const fn public_server_message(self) -> &'static str {
56        match self {
57            Self::ServiceUnavailable => "Service temporarily unavailable",
58            _ => "Internal server error",
59        }
60    }
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct ValidationError {
65    pub field: String,
66    pub message: String,
67    pub code: String,
68    #[serde(skip_serializing_if = "Option::is_none")]
69    // JSON: Validation context echoes the offending request fragment, whatever its shape.
70    pub context: Option<Value>,
71}
72
73/// The HTTP error envelope every non-protocol route answers with.
74#[derive(Debug, Deserialize)]
75pub struct ApiError {
76    pub code: ErrorCode,
77    pub message: String,
78    #[serde(default)]
79    pub details: Option<String>,
80    #[serde(default)]
81    pub error_key: Option<String>,
82    #[serde(default)]
83    pub path: Option<String>,
84    #[serde(default)]
85    pub validation_errors: Vec<ValidationError>,
86    pub timestamp: DateTime<Utc>,
87    #[serde(default)]
88    pub trace_id: Option<TraceId>,
89    #[serde(skip)]
90    source: Option<BoxedSource>,
91}
92
93impl ApiError {
94    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
95        Self {
96            code,
97            message: message.into(),
98            details: None,
99            error_key: None,
100            path: None,
101            validation_errors: Vec::new(),
102            timestamp: Utc::now(),
103            trace_id: None,
104            source: None,
105        }
106    }
107
108    #[must_use]
109    pub fn with_details(mut self, details: impl Into<String>) -> Self {
110        self.details = Some(details.into());
111        self
112    }
113
114    #[must_use]
115    pub fn with_error_key(mut self, key: impl Into<String>) -> Self {
116        self.error_key = Some(key.into());
117        self
118    }
119
120    #[must_use]
121    pub fn with_path(mut self, path: impl Into<String>) -> Self {
122        self.path = Some(path.into());
123        self
124    }
125
126    #[must_use]
127    pub fn with_validation_errors(mut self, errors: Vec<ValidationError>) -> Self {
128        self.validation_errors = errors;
129        self
130    }
131
132    #[must_use]
133    pub fn with_trace_id(mut self, id: TraceId) -> Self {
134        self.trace_id = Some(id);
135        self
136    }
137
138    #[must_use]
139    pub fn with_source(mut self, source: impl Into<BoxedSource>) -> Self {
140        self.source = Some(source.into());
141        self
142    }
143
144    pub fn source(&self) -> Option<&(dyn Error + Send + Sync + 'static)> {
145        self.source.as_deref()
146    }
147
148    pub fn not_found(message: impl Into<String>) -> Self {
149        Self::new(ErrorCode::NotFound, message)
150    }
151
152    pub fn bad_request(message: impl Into<String>) -> Self {
153        Self::new(ErrorCode::BadRequest, message)
154    }
155
156    pub fn unauthorized(message: impl Into<String>) -> Self {
157        Self::new(ErrorCode::Unauthorized, message)
158    }
159
160    pub fn forbidden(message: impl Into<String>) -> Self {
161        Self::new(ErrorCode::Forbidden, message)
162    }
163
164    pub fn conflict(message: impl Into<String>) -> Self {
165        Self::new(ErrorCode::ConflictError, message)
166    }
167
168    pub fn payload_too_large(message: impl Into<String>) -> Self {
169        Self::new(ErrorCode::PayloadTooLarge, message)
170    }
171
172    pub fn locked(message: impl Into<String>) -> Self {
173        Self::new(ErrorCode::Locked, message)
174    }
175
176    pub fn rate_limited(message: impl Into<String>) -> Self {
177        Self::new(ErrorCode::RateLimited, message)
178    }
179
180    pub fn validation_error(message: impl Into<String>, errors: Vec<ValidationError>) -> Self {
181        Self::new(ErrorCode::ValidationError, message).with_validation_errors(errors)
182    }
183
184    pub fn internal(context: &'static str, source: impl Into<BoxedSource>) -> Self {
185        Self::new(ErrorCode::InternalError, context).with_source(source)
186    }
187
188    pub fn service_unavailable(context: &'static str) -> Self {
189        Self::new(ErrorCode::ServiceUnavailable, context)
190    }
191}