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/// 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}