Skip to main content

rp_postgrest/
error.rs

1// Adapted from postgrest-rs / rp-postgrest 2.1.0 (MIT OR Apache-2.0).
2use reqwest::{StatusCode, Url, header::HeaderMap};
3use rp_postgrest_error::{DecodeError, PostgrestError};
4
5/// Configuration rejected before sending a request.
6#[derive(Debug, thiserror::Error)]
7#[non_exhaustive]
8pub enum ConfigError {
9    /// The base URL could not be parsed.
10    #[error("invalid PostgREST URL: {0}")]
11    Url(#[from] url::ParseError),
12    /// Only absolute HTTP(S) URLs without credentials, query or fragment are supported.
13    #[error("PostgREST requires an HTTP(S) base URL without credentials, query or fragment")]
14    InvalidBaseUrl,
15    /// URL parsers normalize dot-only segments; they cannot name a resource safely.
16    #[error("dot-only resource names cannot be represented by the HTTP URL parser")]
17    DotOnlyResource,
18    /// A JSON text path requires at least one key.
19    #[error("JSON text paths require at least one key")]
20    EmptyJsonPath,
21    /// A header name was invalid.
22    #[error("invalid header name: {0}")]
23    HeaderName(#[from] reqwest::header::InvalidHeaderName),
24    /// A header value was invalid.
25    #[error("invalid header value: {0}")]
26    HeaderValue(#[from] reqwest::header::InvalidHeaderValue),
27    /// Prefer directives must be representable as HTTP-visible ASCII.
28    #[error("Prefer directives must contain only HTTP-visible ASCII")]
29    InvalidPreference,
30    /// The default HTTP transport could not be configured.
31    #[error("HTTP client configuration failed: {0}")]
32    Client(#[source] reqwest::Error),
33}
34
35/// Authoritative HTTP metadata retained when a response fails.
36#[derive(Clone, Debug)]
37pub struct ResponseMetadata {
38    status: StatusCode,
39    headers: HeaderMap,
40    url: Url,
41}
42impl ResponseMetadata {
43    pub(crate) fn take_from_response(response: &mut reqwest::Response) -> Self {
44        Self {
45            status: response.status(),
46            headers: core::mem::take(response.headers_mut()),
47            url: response.url().clone(),
48        }
49    }
50    /// Observed HTTP status.
51    #[must_use]
52    pub const fn status(&self) -> StatusCode {
53        self.status
54    }
55    /// Response headers.
56    #[must_use]
57    pub const fn headers(&self) -> &HeaderMap {
58        &self.headers
59    }
60    /// Effective URL after redirects.
61    #[must_use]
62    pub const fn url(&self) -> &Url {
63        &self.url
64    }
65    /// Extracts the server total from Content-Range without counting returned rows.
66    ///
67    /// # Errors
68    /// Rejects missing, unavailable, malformed or overflowing totals.
69    pub fn count(&self) -> Result<u64, crate::CountError> {
70        crate::count::response_count(self.headers())
71    }
72}
73
74/// A configuration, request, or response failure, without nested result layers.
75#[derive(Debug, thiserror::Error)]
76#[non_exhaustive]
77#[expect(
78    clippy::error_impl_error,
79    reason = "Error is the established public protocol error type"
80)]
81pub enum Error {
82    /// Invalid client or request configuration.
83    #[error(transparent)]
84    Configuration(#[from] ConfigError),
85    /// Deferred payload serialization failure.
86    #[error("JSON serialization failed: {0}")]
87    Serialization(#[source] serde_json::Error),
88    /// No usable HTTP response was received.
89    #[error("PostgREST request failed: {0}")]
90    Request(#[source] reqwest::Error),
91    /// Reading a successful or unsuccessful response body failed.
92    #[error("reading PostgREST response body failed: {source}")]
93    ResponseBody {
94        metadata: Box<ResponseMetadata>,
95        source: reqwest::Error,
96    },
97    /// A structured server error; observed HTTP status is authoritative.
98    #[error("{source}")]
99    Postgrest {
100        metadata: Box<ResponseMetadata>,
101        source: Box<PostgrestError>,
102    },
103    /// A malformed error envelope, retaining exact body bytes in its source.
104    #[error("{source}")]
105    Decode {
106        metadata: Box<ResponseMetadata>,
107        source: Box<DecodeError>,
108    },
109    /// A successful response did not match the requested JSON shape.
110    #[error("decoding successful PostgREST response failed: {source}")]
111    ResponseDecode {
112        metadata: Box<ResponseMetadata>,
113        source: serde_json::Error,
114    },
115    /// A requested row count was missing or invalid.
116    #[error("reading PostgREST row count failed: {source}")]
117    Count {
118        metadata: Box<ResponseMetadata>,
119        source: crate::CountError,
120    },
121}
122impl Error {
123    pub(crate) fn decode_server_response(metadata: Box<ResponseMetadata>, body: &[u8]) -> Self {
124        match PostgrestError::from_slice(metadata.status(), body) {
125            Ok(source) => Self::Postgrest {
126                metadata,
127                source: Box::new(source),
128            },
129            Err(source) => Self::Decode {
130                metadata,
131                source: Box::new(source),
132            },
133        }
134    }
135
136    /// Decodes a fixture using the same error-body decoder as checked execution.
137    ///
138    /// Returns `None` for successful HTTP statuses without decoding the body.
139    /// Borrows the body; malformed envelopes copy their bytes into the error.
140    /// Metadata contains empty headers and the synthetic URL `http://localhost/`.
141    ///
142    /// # Panics
143    /// Panics if the constant fixture URL cannot be parsed.
144    #[cfg(feature = "test-util")]
145    #[expect(
146        clippy::expect_used,
147        reason = "The fixture URL is a valid static literal"
148    )]
149    #[must_use]
150    pub fn from_response(status: StatusCode, body: &[u8]) -> Option<Self> {
151        if status.is_success() {
152            return None;
153        }
154        let metadata = Box::new(ResponseMetadata {
155            status,
156            headers: HeaderMap::new(),
157            url: Url::parse("http://localhost/").expect("static URL"),
158        });
159        Some(Self::decode_server_response(metadata, body))
160    }
161
162    /// Observed non-success HTTP status and any decoded server error body.
163    ///
164    /// Malformed envelopes and failed body reads retain their status with no body.
165    /// Successful-response decode and count failures are not server errors.
166    #[must_use]
167    pub fn server_response(
168        &self,
169    ) -> Option<(StatusCode, Option<&rp_postgrest_error::ErrorResponse>)> {
170        let metadata = self.response_metadata()?;
171        if metadata.status().is_success() {
172            return None;
173        }
174        Some((metadata.status(), self.postgrest_body()))
175    }
176
177    /// Whether a JWT validation error specifically reports token expiration.
178    ///
179    /// Other JWT failures, including invalid signatures, return `false`.
180    #[must_use]
181    pub fn is_jwt_expired(&self) -> bool {
182        self.postgrest_body().is_some_and(|body| {
183            matches!(body.code.as_str(), "PGRST301" | "PGRST303")
184                && (body.message.eq_ignore_ascii_case("JWT expired")
185                    || body.message.eq_ignore_ascii_case("JWT expired."))
186        })
187    }
188
189    /// The decoded structured server error without destructuring this error.
190    #[must_use]
191    pub const fn postgrest_error(&self) -> Option<&PostgrestError> {
192        match self {
193            Self::Postgrest { source, .. } => Some(source),
194            Self::Configuration(_)
195            | Self::Serialization(_)
196            | Self::Request(_)
197            | Self::ResponseBody { .. }
198            | Self::Decode { .. }
199            | Self::ResponseDecode { .. }
200            | Self::Count { .. } => None,
201        }
202    }
203    /// The decoded server response, including code, details and hint.
204    #[must_use]
205    pub fn postgrest_body(&self) -> Option<&rp_postgrest_error::ErrorResponse> {
206        self.postgrest_error().map(PostgrestError::response)
207    }
208    /// The authoritative HTTP status and borrowed structured server response.
209    ///
210    /// The status comes from the observed response, not the error code's inferred
211    /// status. Returns `None` when no structured server error was decoded.
212    #[must_use]
213    pub fn postgrest_response(&self) -> Option<(StatusCode, &rp_postgrest_error::ErrorResponse)> {
214        match self {
215            Self::Postgrest { metadata, source } => Some((metadata.status(), source.response())),
216            Self::Configuration(_)
217            | Self::Serialization(_)
218            | Self::Request(_)
219            | Self::ResponseBody { .. }
220            | Self::Decode { .. }
221            | Self::ResponseDecode { .. }
222            | Self::Count { .. } => None,
223        }
224    }
225    /// Metadata when a response was observed.
226    #[must_use]
227    pub const fn response_metadata(&self) -> Option<&ResponseMetadata> {
228        match self {
229            Self::ResponseBody { metadata, .. }
230            | Self::Postgrest { metadata, .. }
231            | Self::Decode { metadata, .. }
232            | Self::ResponseDecode { metadata, .. }
233            | Self::Count { metadata, .. } => Some(metadata),
234            Self::Configuration(_) | Self::Serialization(_) | Self::Request(_) => None,
235        }
236    }
237    /// Authoritative observed HTTP status, if available.
238    #[must_use]
239    pub fn status(&self) -> Option<StatusCode> {
240        self.response_metadata()
241            .map(ResponseMetadata::status)
242            .or_else(|| match self {
243                Self::Request(source) => source.status(),
244                Self::Configuration(_)
245                | Self::Serialization(_)
246                | Self::ResponseBody { .. }
247                | Self::Postgrest { .. }
248                | Self::Decode { .. }
249                | Self::ResponseDecode { .. }
250                | Self::Count { .. } => None,
251            })
252    }
253    /// Effective response URL, or the failed request URL.
254    #[must_use]
255    pub fn url(&self) -> Option<&Url> {
256        self.response_metadata()
257            .map(ResponseMetadata::url)
258            .or_else(|| match self {
259                Self::Request(source) => source.url(),
260                Self::Configuration(_)
261                | Self::Serialization(_)
262                | Self::ResponseBody { .. }
263                | Self::Postgrest { .. }
264                | Self::Decode { .. }
265                | Self::ResponseDecode { .. }
266                | Self::Count { .. } => None,
267            })
268    }
269}