From ddf801456196862ab22752fe3ab530d5c69b2906645c54f6219b0cbb91a9d222 Mon Sep 17 00:00:00 2001 From: Alex Wied <2+alex@noreply.code.graphofliberty.org> Date: Fri, 24 Jul 2026 17:25:38 -0400 Subject: [PATCH] Gracefully handle HTTP 405 errors --- ldp/src/error.rs | 3 +++ ldp/src/resource.rs | 57 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/ldp/src/error.rs b/ldp/src/error.rs index f444c8b..a492714 100644 --- a/ldp/src/error.rs +++ b/ldp/src/error.rs @@ -26,4 +26,7 @@ pub enum Error { /// The RDF data failed to parse. #[error(transparent)] InvalidRdfSyntax(#[from] oxigraph::io::RdfSyntaxError), + + #[error(transparent)] + ToStr(#[from] http::header::ToStrError), } diff --git a/ldp/src/resource.rs b/ldp/src/resource.rs index 48d31a2..974b64a 100644 --- a/ldp/src/resource.rs +++ b/ldp/src/resource.rs @@ -2,6 +2,7 @@ use crate::rdf_source::RdfSource; use crate::vocab; use bytes::Bytes; use futures::Stream; +use http::Method; use oxigraph::io::{RdfFormat, RdfParser}; use oxigraph::model::{GraphNameRef, NamedNodeRef, Quad}; use reqwest_middleware::reqwest::{Client, Response, StatusCode, Url, header}; @@ -34,6 +35,7 @@ pub struct ResourceRequestBuilder { client: ClientWithMiddleware, url: Url, follow_described_by: bool, + validate_support: bool, formats: Vec, } @@ -52,17 +54,30 @@ impl ResourceRequestBuilder { client, url, follow_described_by: true, + validate_support: true, 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. + /// + /// Default value: `true`. #[must_use] pub fn follow_described_by(mut self, value: bool) -> Self { self.follow_described_by = value; self } + /// Require the presence of a `Link: ; 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. /// /// Repeated calls are additive. By default, all formats — even non-RDF formats — are accepted. @@ -150,19 +165,48 @@ impl ResourceRequest { request_builder } + fn is_method_allowed(response: &Response, method: Method) -> crate::Result { + 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. /// - /// 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 user permits it, then the URL in the /// [describedby](https://www.w3.org/TR/ldp/#link-relation-describedby) header 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 { let request_builder = self.builder.client.head(self.builder.url.clone()); - let mut response = request_builder.send().await?.error_for_status()?; - Self::ensure_ldp_support(&response)?; + 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)?; + } + } + 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 described_by = Self::extract_described_by(&response); @@ -178,7 +222,10 @@ impl ResourceRequest { request_builder = self.add_media_types(request_builder); response = request_builder.send().await?.error_for_status()?; - Self::ensure_ldp_support(&response)?; + + if self.builder.validate_support { + Self::ensure_ldp_support(&response)?; + } let state_token = response .headers()