1use reqwest::{StatusCode, Url, header::HeaderMap};
3use rp_postgrest_error::{DecodeError, PostgrestError};
4
5#[derive(Debug, thiserror::Error)]
7#[non_exhaustive]
8pub enum ConfigError {
9 #[error("invalid PostgREST URL: {0}")]
11 Url(#[from] url::ParseError),
12 #[error("PostgREST requires an HTTP(S) base URL without credentials, query or fragment")]
14 InvalidBaseUrl,
15 #[error("dot-only resource names cannot be represented by the HTTP URL parser")]
17 DotOnlyResource,
18 #[error("JSON text paths require at least one key")]
20 EmptyJsonPath,
21 #[error("invalid header name: {0}")]
23 HeaderName(#[from] reqwest::header::InvalidHeaderName),
24 #[error("invalid header value: {0}")]
26 HeaderValue(#[from] reqwest::header::InvalidHeaderValue),
27 #[error("Prefer directives must contain only HTTP-visible ASCII")]
29 InvalidPreference,
30 #[error("HTTP client configuration failed: {0}")]
32 Client(#[source] reqwest::Error),
33}
34
35#[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 #[must_use]
52 pub const fn status(&self) -> StatusCode {
53 self.status
54 }
55 #[must_use]
57 pub const fn headers(&self) -> &HeaderMap {
58 &self.headers
59 }
60 #[must_use]
62 pub const fn url(&self) -> &Url {
63 &self.url
64 }
65 pub fn count(&self) -> Result<u64, crate::CountError> {
70 crate::count::response_count(self.headers())
71 }
72}
73
74#[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 #[error(transparent)]
84 Configuration(#[from] ConfigError),
85 #[error("JSON serialization failed: {0}")]
87 Serialization(#[source] serde_json::Error),
88 #[error("PostgREST request failed: {0}")]
90 Request(#[source] reqwest::Error),
91 #[error("reading PostgREST response body failed: {source}")]
93 ResponseBody {
94 metadata: Box<ResponseMetadata>,
95 source: reqwest::Error,
96 },
97 #[error("{source}")]
99 Postgrest {
100 metadata: Box<ResponseMetadata>,
101 source: Box<PostgrestError>,
102 },
103 #[error("{source}")]
105 Decode {
106 metadata: Box<ResponseMetadata>,
107 source: Box<DecodeError>,
108 },
109 #[error("decoding successful PostgREST response failed: {source}")]
111 ResponseDecode {
112 metadata: Box<ResponseMetadata>,
113 source: serde_json::Error,
114 },
115 #[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 #[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 #[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 #[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 #[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 #[must_use]
205 pub fn postgrest_body(&self) -> Option<&rp_postgrest_error::ErrorResponse> {
206 self.postgrest_error().map(PostgrestError::response)
207 }
208 #[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 #[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 #[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 #[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}