1use crate::response::{IntoResponse, Response, with_content_type};
4use http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
5use serde::Serialize;
6use serde_json::Value;
7use siderite_openapi::{Operation, Schema, SchemaObject, SchemaRegistry};
8use siderite_orm::{BackendError, OrmError, QueryError};
9use std::fmt::Display;
10use thiserror::Error;
11
12pub const PROBLEM_JSON: &str = "application/problem+json";
14
15const GENERIC_INTERNAL_DETAIL: &str = "An internal error occurred.";
16const FALLBACK_BODY: &[u8] =
17 br#"{"type":"about:blank","title":"Internal Server Error","status":500}"#;
18
19pub type ApiResult<T> = Result<T, ApiError>;
21
22#[derive(Debug, Clone, Error)]
24#[error("{status}: {title}")]
25pub struct ApiError {
26 status: StatusCode,
27 title: &'static str,
28 type_uri: String,
29 detail: Option<String>,
30 errors: Option<Box<Value>>,
33 headers: Option<Box<HeaderMap>>,
36 absent: bool,
39 code: Option<&'static str>,
41}
42
43#[derive(Serialize)]
44struct Problem<'a> {
45 #[serde(rename = "type")]
46 type_uri: &'a str,
47 title: &'a str,
48 status: u16,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 detail: Option<&'a str>,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 errors: Option<&'a Value>,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 code: Option<&'a str>,
55}
56
57impl ApiError {
58 pub fn new(status: StatusCode, detail: impl Into<String>) -> Self {
60 Self {
61 status,
62 title: status.canonical_reason().unwrap_or("Unknown"),
63 type_uri: "about:blank".to_owned(),
64 detail: Some(detail.into()),
65 errors: None,
66 headers: None,
67 absent: false,
68 code: None,
69 }
70 }
71
72 pub fn not_found(detail: impl Into<String>) -> Self {
74 Self::new(StatusCode::NOT_FOUND, detail)
75 }
76
77 pub fn bad_request(detail: impl Into<String>) -> Self {
79 Self::new(StatusCode::BAD_REQUEST, detail)
80 }
81
82 pub fn unprocessable(errors: Value) -> Self {
84 let mut err = Self::new(
85 StatusCode::UNPROCESSABLE_ENTITY,
86 "The request could not be processed; see `errors` for details.",
87 );
88 err.errors = Some(Box::new(errors));
89 err
90 }
91
92 pub fn internal(source: impl Display) -> Self {
94 tracing::error!(error = %source, "internal server error");
95 Self::new(StatusCode::INTERNAL_SERVER_ERROR, GENERIC_INTERNAL_DETAIL)
96 }
97
98 #[must_use]
100 pub fn with_type(mut self, type_uri: impl Into<String>) -> Self {
101 self.type_uri = type_uri.into();
102 self
103 }
104
105 #[must_use]
108 pub fn with_code(mut self, code: &'static str) -> Self {
109 self.code = Some(code);
110 self
111 }
112
113 #[must_use]
115 pub fn with_header(mut self, name: HeaderName, value: HeaderValue) -> Self {
116 self.headers
117 .get_or_insert_with(Default::default)
118 .append(name, value);
119 self
120 }
121
122 #[must_use]
128 pub fn absent(mut self) -> Self {
129 self.absent = true;
130 self
131 }
132
133 pub fn is_absent(&self) -> bool {
135 self.absent
136 }
137
138 pub fn headers(&self) -> Option<&HeaderMap> {
140 self.headers.as_deref()
141 }
142
143 pub fn status(&self) -> StatusCode {
145 self.status
146 }
147
148 pub fn title(&self) -> &str {
150 self.title
151 }
152
153 pub fn detail(&self) -> Option<&str> {
155 self.detail.as_deref()
156 }
157
158 pub fn errors(&self) -> Option<&Value> {
160 self.errors.as_deref()
161 }
162
163 pub fn code(&self) -> Option<&str> {
165 self.code
166 }
167}
168
169impl IntoResponse for ApiError {
170 fn into_response(self) -> Response {
171 let problem = Problem {
172 type_uri: &self.type_uri,
173 title: self.title,
174 status: self.status.as_u16(),
175 detail: self.detail.as_deref(),
176 errors: self.errors.as_deref(),
177 code: self.code,
178 };
179 let body = serde_json::to_vec(&problem).unwrap_or_else(|_| FALLBACK_BODY.to_vec());
180 let mut response = with_content_type(PROBLEM_JSON, body);
181 *response.status_mut() = self.status;
182 if let Some(extra) = self.headers {
183 response.headers_mut().extend(*extra);
184 }
185 response
186 }
187
188 fn describe(op: &mut Operation, registry: &mut SchemaRegistry) {
189 let schema = registry.subschema::<ProblemSchema>();
190 op.add_response(
191 "default",
192 "Error (RFC 7807 problem details)",
193 Some((PROBLEM_JSON, schema)),
194 );
195 }
196}
197
198struct ProblemSchema;
200
201impl Schema for ProblemSchema {
202 fn schema_name() -> Option<&'static str> {
203 Some("Problem")
204 }
205
206 fn schema(_: &mut SchemaRegistry) -> SchemaObject {
207 SchemaObject::of_type("object")
208 .with(
209 "properties",
210 serde_json::json!({
211 "type": {"type": "string"},
212 "title": {"type": "string"},
213 "status": {"type": "integer"},
214 "detail": {"type": "string"},
215 "errors": {},
216 "code": {"type": "string"}
217 }),
218 )
219 .with("required", serde_json::json!(["type", "title", "status"]))
220 }
221}
222
223impl From<OrmError> for ApiError {
224 fn from(err: OrmError) -> Self {
225 match err {
226 OrmError::Query(QueryError::DoesNotExist) => Self::not_found("Resource not found."),
227 OrmError::Cursor(cursor) => {
228 let detail = "The pagination cursor is not valid.";
230 Self::bad_request(detail).with_code(cursor.code())
231 }
232 OrmError::Capability(cap) => {
233 tracing::error!(error = %cap, "unsupported backend capability");
234 Self::new(
235 StatusCode::NOT_IMPLEMENTED,
236 "The requested operation is not supported by the configured database backend.",
237 )
238 }
239 OrmError::Backend(BackendError::Constraint(detail)) => {
240 tracing::info!(%detail, "database constraint violation");
242 Self::new(
243 StatusCode::CONFLICT,
244 "The request conflicts with existing data (a uniqueness, reference or check constraint failed).",
245 )
246 }
247 other => Self::internal(other),
248 }
249 }
250}
251
252#[derive(Debug, Error)]
254#[non_exhaustive]
255pub enum ServerError {
256 #[error("failed to bind {addr}")]
258 Bind {
259 addr: String,
261 #[source]
263 source: std::io::Error,
264 },
265 #[error("invalid application configuration: {0}")]
267 Configuration(String),
268 #[error("lifespan hook failed: {0}")]
270 Lifespan(#[source] ApiError),
271 #[error("server error: {0}")]
273 Serve(#[source] std::io::Error),
274 #[error("application shutdown exceeded its deadline")]
276 ShutdownTimeout,
277}
278
279#[derive(Debug, Clone, Error)]
281#[error("{message}")]
282pub struct BodyError {
283 message: String,
284 too_large: bool,
285}
286
287impl BodyError {
288 pub(crate) fn failed(reason: String) -> Self {
289 Self {
290 message: format!("failed to read body: {reason}"),
291 too_large: false,
292 }
293 }
294
295 pub(crate) fn too_large(max_bytes: usize) -> Self {
296 Self {
297 message: format!("body exceeds the limit of {max_bytes} bytes"),
298 too_large: true,
299 }
300 }
301
302 pub fn is_too_large(&self) -> bool {
304 self.too_large
305 }
306}
307
308impl From<BodyError> for ApiError {
309 fn from(err: BodyError) -> Self {
310 if err.too_large {
311 ApiError::new(
312 StatusCode::PAYLOAD_TOO_LARGE,
313 format!("Request {}.", err.message),
314 )
315 } else {
316 ApiError::bad_request(err.message)
317 }
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 #[test]
326 fn orm_errors_map_to_http_statuses() {
327 let conflict = ApiError::from(OrmError::Backend(BackendError::Constraint(
328 "UNIQUE constraint failed: users.email".into(),
329 )));
330 assert_eq!(conflict.status(), StatusCode::CONFLICT);
331 let missing = ApiError::from(OrmError::Query(QueryError::DoesNotExist));
332 assert_eq!(missing.status(), StatusCode::NOT_FOUND);
333 let broken = ApiError::from(OrmError::Backend(BackendError::Database("x".into())));
334 assert_eq!(broken.status(), StatusCode::INTERNAL_SERVER_ERROR);
335 let foreign = siderite_orm::CursorError::ForeignQuery;
336 let stale = ApiError::from(OrmError::Cursor(foreign));
337 assert_eq!(stale.status(), StatusCode::BAD_REQUEST);
338 assert_eq!(stale.code(), Some("cursor.E003"));
339 let detail = Some("The pagination cursor is not valid.");
340 assert_eq!(stale.detail(), detail);
341 }
342}