postrust_server/error.rs
1//! The error a route of your own answers with, in PostgREST's shape.
2//!
3//! A client of the API branches on PostgREST's error body -- `code`,
4//! `message`, `details`, `hint` -- and on the status that comes with it. A
5//! route nested beside the API is answered by the same client, so when it
6//! fails it should fail in the same words: a token that does not verify is
7//! the same 401, a unique violation from its own `INSERT` is the same 409.
8//! [`Error`] is that. It is what [`User`](crate::User) rejects a request
9//! with, what [`AppState::begin_as`](crate::AppState::begin_as) returns, and
10//! what `?` makes of a `sqlx::Error` in a handler that returns it.
11
12use crate::state::AppState;
13use axum::extract::State;
14use axum::response::{IntoResponse, Response};
15use http::StatusCode;
16use std::sync::{Arc, Mutex};
17
18/// A failure answered as the REST API would answer it.
19///
20/// Wraps the [`postrust_core::Error`] the REST pipeline reports with, so its
21/// status and body come from the same table: a database error by its
22/// SQLSTATE (`23505` is 409, `42501` a missing grant is 401, `P0001` a bare
23/// `RAISE` is 400), a refusal raised with `sqlstate 'PGRST'` with the status,
24/// headers and body the function wrote, a broken connection as 503.
25///
26/// The database's own account of an error -- its message, detail and hint,
27/// which name constraints and columns -- is passed through only where the
28/// REST API would pass it through: in compatibility mode (`PGRST_COMPAT_MODE`),
29/// for a SQLSTATE `PGRST_DB_ERRORS_VERBATIM` lists, or with `PGRST_DEBUG`. The
30/// first two are the server's configuration, which an error converted by `?`
31/// has no way to reach, so they are applied on the way
32/// out by the routers given to [`Builder::nest`](crate::Builder::nest). A
33/// router served some other way gets the sanitised body.
34pub struct Error(Box<postrust_core::Error>);
35
36impl Error {
37 /// The status this is answered with.
38 pub fn status(&self) -> StatusCode {
39 self.0.status_code()
40 }
41
42 /// The error as the REST pipeline reports it.
43 pub fn as_core(&self) -> &postrust_core::Error {
44 &self.0
45 }
46
47 /// The error as the REST pipeline reports it, by value.
48 pub fn into_core(self) -> postrust_core::Error {
49 *self.0
50 }
51}
52
53impl std::fmt::Debug for Error {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 self.0.fmt(f)
56 }
57}
58
59impl std::fmt::Display for Error {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 self.0.fmt(f)
62 }
63}
64
65impl std::error::Error for Error {
66 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
67 Some(&*self.0)
68 }
69}
70
71impl From<postrust_core::Error> for Error {
72 fn from(error: postrust_core::Error) -> Self {
73 Self(Box::new(error))
74 }
75}
76
77impl From<postrust_core::db::DbError> for Error {
78 fn from(error: postrust_core::db::DbError) -> Self {
79 postrust_core::Error::from(error).into()
80 }
81}
82
83/// Through the driver-neutral [`DbError`](postrust_core::db::DbError), as the
84/// REST pipeline's own sqlx errors go: a statement PostgreSQL refused keeps
85/// its SQLSTATE, a connection that broke or could not be had from the pool is
86/// the database being unavailable, anything else is internal.
87impl From<sqlx::Error> for Error {
88 fn from(error: sqlx::Error) -> Self {
89 postrust_core::db::DbError::from(error).into()
90 }
91}
92
93impl IntoResponse for Error {
94 fn into_response(self) -> Response {
95 // Logged as the REST pipeline logs a query: a fault -- a 5xx, which
96 // is the operator's to look at -- at error, and a refusal the client
97 // brought on itself at debug, where it does not bury the faults.
98 if self.status().is_server_error() {
99 tracing::error!("Custom route error: {}", self.0);
100 } else {
101 tracing::debug!("Custom route refused with {}: {}", self.status(), self.0);
102 }
103
104 // Rendered sanitised, which is right unless the server passes
105 // database errors through (compatibility mode, or
106 // `PGRST_DB_ERRORS_VERBATIM`). A database error -- the only kind
107 // either renders differently -- rides along in the extensions as well,
108 // so that `render_verbatim`, which can see the configuration, can
109 // render it again where it is. Never sent: extensions are not
110 // serialised.
111 let kept = match &*self.0 {
112 postrust_core::Error::Database(db) => Some(Unrendered(Arc::new(Mutex::new(Some(
113 postrust_core::Error::Database(copy_of(db)),
114 ))))),
115 _ => None,
116 };
117 let mut response = postrust_engine::error_response(*self.0, false);
118 if let Some(kept) = kept {
119 response.extensions_mut().insert(kept);
120 }
121 response.map(axum::body::Body::from)
122 }
123}
124
125/// `DatabaseError` is not `Clone`; every field is a string.
126fn copy_of(db: &postrust_core::error::DatabaseError) -> postrust_core::error::DatabaseError {
127 postrust_core::error::DatabaseError {
128 code: db.code.clone(),
129 message: db.message.clone(),
130 details: db.details.clone(),
131 hint: db.hint.clone(),
132 constraint: db.constraint.clone(),
133 table: db.table.clone(),
134 column: db.column.clone(),
135 }
136}
137
138/// A database error a response was rendered from, sanitised, kept for
139/// [`render_verbatim`]. Only database errors are kept: passing them through
140/// changes how nothing else is rendered.
141#[derive(Clone)]
142struct Unrendered(Arc<Mutex<Option<postrust_core::Error>>>);
143
144/// Render a route's [`Error`] again with the database's own words, where the
145/// configuration passes its SQLSTATE through.
146///
147/// A layer on each router given to [`Builder::nest`](crate::Builder::nest)
148/// when compatibility mode is on or `PGRST_DB_ERRORS_VERBATIM` lists any
149/// SQLSTATE, and only then.
150pub(crate) async fn render_verbatim(
151 State(state): State<Arc<AppState>>,
152 mut response: Response,
153) -> Response {
154 let Some(Unrendered(error)) = response.extensions_mut().remove::<Unrendered>() else {
155 return response;
156 };
157 let error = error.lock().ok().and_then(|mut error| error.take());
158 match error {
159 // Only the body: for a database error that is all compatibility mode
160 // changes, and the status and headers are kept as they left the
161 // route -- including any a layer of the route's own added.
162 Some(error) => {
163 let (_, body) = postrust_engine::error_response_for(&state.config, error).into_parts();
164 let (parts, _) = response.into_parts();
165 Response::from_parts(parts, axum::body::Body::from(body))
166 }
167 None => response,
168 }
169}