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/// or with `PGRST_DEBUG`. Compatibility mode is the server's configuration, which
30/// an error converted by `?` has no way to reach, so it is applied on the way
31/// out by the routers given to [`Builder::nest`](crate::Builder::nest). A
32/// router served some other way gets the sanitised body.
33pub struct Error(Box<postrust_core::Error>);
34
35impl Error {
36 /// The status this is answered with.
37 pub fn status(&self) -> StatusCode {
38 self.0.status_code()
39 }
40
41 /// The error as the REST pipeline reports it.
42 pub fn as_core(&self) -> &postrust_core::Error {
43 &self.0
44 }
45
46 /// The error as the REST pipeline reports it, by value.
47 pub fn into_core(self) -> postrust_core::Error {
48 *self.0
49 }
50}
51
52impl std::fmt::Debug for Error {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 self.0.fmt(f)
55 }
56}
57
58impl std::fmt::Display for Error {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 self.0.fmt(f)
61 }
62}
63
64impl std::error::Error for Error {
65 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
66 Some(&*self.0)
67 }
68}
69
70impl From<postrust_core::Error> for Error {
71 fn from(error: postrust_core::Error) -> Self {
72 Self(Box::new(error))
73 }
74}
75
76impl From<postrust_core::db::DbError> for Error {
77 fn from(error: postrust_core::db::DbError) -> Self {
78 postrust_core::Error::from(error).into()
79 }
80}
81
82/// Through the driver-neutral [`DbError`](postrust_core::db::DbError), as the
83/// REST pipeline's own sqlx errors go: a statement PostgreSQL refused keeps
84/// its SQLSTATE, a connection that broke or could not be had from the pool is
85/// the database being unavailable, anything else is internal.
86impl From<sqlx::Error> for Error {
87 fn from(error: sqlx::Error) -> Self {
88 postrust_core::db::DbError::from(error).into()
89 }
90}
91
92impl IntoResponse for Error {
93 fn into_response(self) -> Response {
94 // Logged as the REST pipeline logs a query: a fault -- a 5xx, which
95 // is the operator's to look at -- at error, and a refusal the client
96 // brought on itself at debug, where it does not bury the faults.
97 if self.status().is_server_error() {
98 tracing::error!("Custom route error: {}", self.0);
99 } else {
100 tracing::debug!("Custom route refused with {}: {}", self.status(), self.0);
101 }
102
103 // Rendered sanitised, which is right unless the server is in
104 // compatibility mode. A database error -- the only kind compatibility
105 // mode renders differently -- rides along in the extensions as well,
106 // so that `render_verbatim`, which can see the configuration, can
107 // render it again where it is. Never sent: extensions are not
108 // serialised.
109 let kept = match &*self.0 {
110 postrust_core::Error::Database(db) => Some(Unrendered(Arc::new(Mutex::new(Some(
111 postrust_core::Error::Database(copy_of(db)),
112 ))))),
113 _ => None,
114 };
115 let mut response = postrust_engine::error_response(*self.0, false);
116 if let Some(kept) = kept {
117 response.extensions_mut().insert(kept);
118 }
119 response.map(axum::body::Body::from)
120 }
121}
122
123/// `DatabaseError` is not `Clone`; every field is a string.
124fn copy_of(db: &postrust_core::error::DatabaseError) -> postrust_core::error::DatabaseError {
125 postrust_core::error::DatabaseError {
126 code: db.code.clone(),
127 message: db.message.clone(),
128 details: db.details.clone(),
129 hint: db.hint.clone(),
130 constraint: db.constraint.clone(),
131 table: db.table.clone(),
132 column: db.column.clone(),
133 }
134}
135
136/// A database error a response was rendered from, sanitised, kept for
137/// [`render_verbatim`]. Only database errors are kept: compatibility mode
138/// changes how nothing else is rendered.
139#[derive(Clone)]
140struct Unrendered(Arc<Mutex<Option<postrust_core::Error>>>);
141
142/// Render a route's [`Error`] again with the database's own words, in
143/// compatibility mode.
144///
145/// A layer on each router given to [`Builder::nest`](crate::Builder::nest)
146/// when compatibility mode is on, and only then.
147pub(crate) async fn render_verbatim(
148 State(state): State<Arc<AppState>>,
149 mut response: Response,
150) -> Response {
151 let Some(Unrendered(error)) = response.extensions_mut().remove::<Unrendered>() else {
152 return response;
153 };
154 let error = error.lock().ok().and_then(|mut error| error.take());
155 match error {
156 // Only the body: for a database error that is all compatibility mode
157 // changes, and the status and headers are kept as they left the
158 // route -- including any a layer of the route's own added.
159 Some(error) => {
160 let (_, body) =
161 postrust_engine::error_response(error, state.config.compat_mode).into_parts();
162 let (parts, _) = response.into_parts();
163 Response::from_parts(parts, axum::body::Body::from(body))
164 }
165 None => response,
166 }
167}