Gracefully handle HTTP 405 errors

This commit is contained in:
2026-07-24 17:27:59 -04:00
parent 3be41f1733
commit ddf8014561
2 changed files with 55 additions and 5 deletions
+3
View File
@@ -26,4 +26,7 @@ pub enum Error {
/// The RDF data failed to parse. /// The RDF data failed to parse.
#[error(transparent)] #[error(transparent)]
InvalidRdfSyntax(#[from] oxigraph::io::RdfSyntaxError), InvalidRdfSyntax(#[from] oxigraph::io::RdfSyntaxError),
#[error(transparent)]
ToStr(#[from] http::header::ToStrError),
} }
+50 -3
View File
@@ -2,6 +2,7 @@ use crate::rdf_source::RdfSource;
use crate::vocab; use crate::vocab;
use bytes::Bytes; use bytes::Bytes;
use futures::Stream; use futures::Stream;
use http::Method;
use oxigraph::io::{RdfFormat, RdfParser}; use oxigraph::io::{RdfFormat, RdfParser};
use oxigraph::model::{GraphNameRef, NamedNodeRef, Quad}; use oxigraph::model::{GraphNameRef, NamedNodeRef, Quad};
use reqwest_middleware::reqwest::{Client, Response, StatusCode, Url, header}; use reqwest_middleware::reqwest::{Client, Response, StatusCode, Url, header};
@@ -34,6 +35,7 @@ pub struct ResourceRequestBuilder {
client: ClientWithMiddleware, client: ClientWithMiddleware,
url: Url, url: Url,
follow_described_by: bool, follow_described_by: bool,
validate_support: bool,
formats: Vec<RdfFormat>, formats: Vec<RdfFormat>,
} }
@@ -52,17 +54,30 @@ impl ResourceRequestBuilder {
client, client,
url, url,
follow_described_by: true, follow_described_by: true,
validate_support: true,
formats: Vec::new(), formats: Vec::new(),
} }
} }
/// If the HTTP response includes a [describedby](https://www.w3.org/TR/ldp/#link-relation-describedby) link rel, then it will be followed. /// If the HTTP response includes a [describedby](https://www.w3.org/TR/ldp/#link-relation-describedby) link rel, then it will be followed.
///
/// Default value: `true`.
#[must_use] #[must_use]
pub fn follow_described_by(mut self, value: bool) -> Self { pub fn follow_described_by(mut self, value: bool) -> Self {
self.follow_described_by = value; self.follow_described_by = value;
self self
} }
/// Require the presence of a `Link: <http://www.w3.org/ns/ldp#Resource>; rel="type"` response header.
/// This is described in [section 4.2.1.4](https://www.w3.org/TR/ldp/#ldpr-resource) of the spec.
///
/// Default value: `true`.
#[must_use]
pub fn validate_support(mut self, value: bool) -> Self {
self.validate_support = value;
self
}
/// Restrict the request to the given RDF format. /// Restrict the request to the given RDF format.
/// ///
/// Repeated calls are additive. By default, all formats — even non-RDF formats — are accepted. /// Repeated calls are additive. By default, all formats — even non-RDF formats — are accepted.
@@ -150,19 +165,48 @@ impl ResourceRequest {
request_builder request_builder
} }
fn is_method_allowed(response: &Response, method: Method) -> crate::Result<bool> {
for header in response.headers().get_all(header::ALLOW) {
for value in header.to_str()?.replace(" ", "").split(',') {
if value == method {
return Ok(true);
}
}
}
Ok(false)
}
/// Send the request. /// Send the request.
/// ///
/// There are two stages to this process. First, a HEAD request is made to determine whether /// There are two stages to this process. First, a `HEAD` request is made to determine whether
/// the requested resource is described by an RDF graph at another location. If it is, and if /// the requested resource is described by an RDF graph at another location. If it is, and if
/// the user permits it, then the URL in the /// the user permits it, then the URL in the
/// [describedby](https://www.w3.org/TR/ldp/#link-relation-describedby) header is used. /// [describedby](https://www.w3.org/TR/ldp/#link-relation-describedby) header is used.
/// Otherwise, the original URL is used. /// Otherwise, the original URL is used.
/// ///
/// During the second stage, a GET request is made. No parsing occurs at this time. /// During the second stage, a `GET` request is made. No parsing occurs at this time.
///
/// If the `HEAD` method is not allowed on this URI (a violation of the LDP spec), the `Allow`
/// header is inspected for `GET`. If absent, this method will return an error.
pub async fn send(&self) -> crate::Result<Resource> { pub async fn send(&self) -> crate::Result<Resource> {
let request_builder = self.builder.client.head(self.builder.url.clone()); let request_builder = self.builder.client.head(self.builder.url.clone());
let mut response = request_builder.send().await?.error_for_status()?; let mut response = request_builder.send().await?;
match response.error_for_status_ref() {
Ok(response) => {
if self.builder.validate_support {
Self::ensure_ldp_support(&response)?; Self::ensure_ldp_support(&response)?;
}
}
Err(err) if err.status() == Some(StatusCode::METHOD_NOT_ALLOWED) => {
if !Self::is_method_allowed(&response, Method::GET)? {
return Err(err.into());
}
}
err => {
err?;
}
}
let url_to_get; let url_to_get;
let described_by = Self::extract_described_by(&response); let described_by = Self::extract_described_by(&response);
@@ -178,7 +222,10 @@ impl ResourceRequest {
request_builder = self.add_media_types(request_builder); request_builder = self.add_media_types(request_builder);
response = request_builder.send().await?.error_for_status()?; response = request_builder.send().await?.error_for_status()?;
if self.builder.validate_support {
Self::ensure_ldp_support(&response)?; Self::ensure_ldp_support(&response)?;
}
let state_token = response let state_token = response
.headers() .headers()