use reqwest::{StatusCode, Url, header::HeaderMap};
use rp_postgrest_error::{DecodeError, PostgrestError};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ConfigError {
#[error("invalid PostgREST URL: {0}")]
Url(#[from] url::ParseError),
#[error("PostgREST requires an HTTP(S) base URL without credentials, query or fragment")]
InvalidBaseUrl,
#[error("dot-only resource names cannot be represented by the HTTP URL parser")]
DotOnlyResource,
#[error("JSON text paths require at least one key")]
EmptyJsonPath,
#[error("invalid header name: {0}")]
HeaderName(#[from] reqwest::header::InvalidHeaderName),
#[error("invalid header value: {0}")]
HeaderValue(#[from] reqwest::header::InvalidHeaderValue),
#[error("Prefer directives must contain only HTTP-visible ASCII")]
InvalidPreference,
#[error("HTTP client configuration failed: {0}")]
Client(#[source] reqwest::Error),
}
#[derive(Clone, Debug)]
pub struct ResponseMetadata {
status: StatusCode,
headers: HeaderMap,
url: Url,
}
impl ResponseMetadata {
pub(crate) fn take_from_response(response: &mut reqwest::Response) -> Self {
Self {
status: response.status(),
headers: core::mem::take(response.headers_mut()),
url: response.url().clone(),
}
}
#[must_use]
pub const fn status(&self) -> StatusCode {
self.status
}
#[must_use]
pub const fn headers(&self) -> &HeaderMap {
&self.headers
}
#[must_use]
pub const fn url(&self) -> &Url {
&self.url
}
pub fn count(&self) -> Result<u64, crate::CountError> {
crate::count::response_count(self.headers())
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
#[expect(
clippy::error_impl_error,
reason = "Error is the established public protocol error type"
)]
pub enum Error {
#[error(transparent)]
Configuration(#[from] ConfigError),
#[error("JSON serialization failed: {0}")]
Serialization(#[source] serde_json::Error),
#[error("PostgREST request failed: {0}")]
Request(#[source] reqwest::Error),
#[error("reading PostgREST response body failed: {source}")]
ResponseBody {
metadata: Box<ResponseMetadata>,
source: reqwest::Error,
},
#[error("{source}")]
Postgrest {
metadata: Box<ResponseMetadata>,
source: Box<PostgrestError>,
},
#[error("{source}")]
Decode {
metadata: Box<ResponseMetadata>,
source: Box<DecodeError>,
},
#[error("decoding successful PostgREST response failed: {source}")]
ResponseDecode {
metadata: Box<ResponseMetadata>,
source: serde_json::Error,
},
#[error("reading PostgREST row count failed: {source}")]
Count {
metadata: Box<ResponseMetadata>,
source: crate::CountError,
},
}
impl Error {
pub(crate) fn decode_server_response(metadata: Box<ResponseMetadata>, body: &[u8]) -> Self {
match PostgrestError::from_slice(metadata.status(), body) {
Ok(source) => Self::Postgrest {
metadata,
source: Box::new(source),
},
Err(source) => Self::Decode {
metadata,
source: Box::new(source),
},
}
}
#[cfg(feature = "test-util")]
#[expect(
clippy::expect_used,
reason = "The fixture URL is a valid static literal"
)]
#[must_use]
pub fn from_response(status: StatusCode, body: &[u8]) -> Option<Self> {
if status.is_success() {
return None;
}
let metadata = Box::new(ResponseMetadata {
status,
headers: HeaderMap::new(),
url: Url::parse("http://localhost/").expect("static URL"),
});
Some(Self::decode_server_response(metadata, body))
}
#[must_use]
pub fn server_response(
&self,
) -> Option<(StatusCode, Option<&rp_postgrest_error::ErrorResponse>)> {
let metadata = self.response_metadata()?;
if metadata.status().is_success() {
return None;
}
Some((metadata.status(), self.postgrest_body()))
}
#[must_use]
pub fn is_jwt_expired(&self) -> bool {
self.postgrest_body().is_some_and(|body| {
matches!(body.code.as_str(), "PGRST301" | "PGRST303")
&& (body.message.eq_ignore_ascii_case("JWT expired")
|| body.message.eq_ignore_ascii_case("JWT expired."))
})
}
#[must_use]
pub const fn postgrest_error(&self) -> Option<&PostgrestError> {
match self {
Self::Postgrest { source, .. } => Some(source),
Self::Configuration(_)
| Self::Serialization(_)
| Self::Request(_)
| Self::ResponseBody { .. }
| Self::Decode { .. }
| Self::ResponseDecode { .. }
| Self::Count { .. } => None,
}
}
#[must_use]
pub fn postgrest_body(&self) -> Option<&rp_postgrest_error::ErrorResponse> {
self.postgrest_error().map(PostgrestError::response)
}
#[must_use]
pub fn postgrest_response(&self) -> Option<(StatusCode, &rp_postgrest_error::ErrorResponse)> {
match self {
Self::Postgrest { metadata, source } => Some((metadata.status(), source.response())),
Self::Configuration(_)
| Self::Serialization(_)
| Self::Request(_)
| Self::ResponseBody { .. }
| Self::Decode { .. }
| Self::ResponseDecode { .. }
| Self::Count { .. } => None,
}
}
#[must_use]
pub const fn response_metadata(&self) -> Option<&ResponseMetadata> {
match self {
Self::ResponseBody { metadata, .. }
| Self::Postgrest { metadata, .. }
| Self::Decode { metadata, .. }
| Self::ResponseDecode { metadata, .. }
| Self::Count { metadata, .. } => Some(metadata),
Self::Configuration(_) | Self::Serialization(_) | Self::Request(_) => None,
}
}
#[must_use]
pub fn status(&self) -> Option<StatusCode> {
self.response_metadata()
.map(ResponseMetadata::status)
.or_else(|| match self {
Self::Request(source) => source.status(),
Self::Configuration(_)
| Self::Serialization(_)
| Self::ResponseBody { .. }
| Self::Postgrest { .. }
| Self::Decode { .. }
| Self::ResponseDecode { .. }
| Self::Count { .. } => None,
})
}
#[must_use]
pub fn url(&self) -> Option<&Url> {
self.response_metadata()
.map(ResponseMetadata::url)
.or_else(|| match self {
Self::Request(source) => source.url(),
Self::Configuration(_)
| Self::Serialization(_)
| Self::ResponseBody { .. }
| Self::Postgrest { .. }
| Self::Decode { .. }
| Self::ResponseDecode { .. }
| Self::Count { .. } => None,
})
}
}