Skip to main content

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}