use http::Method;
use http_extensions::routing::{Router, RouterContext};
use http_extensions::{HttpRequest, HttpRequestExt};
use seatbelt::{Attempt, RecoveryInfo};
#[derive(Debug, Default)]
pub struct HttpClone(Inner);
impl HttpClone {
#[must_use]
pub fn all() -> Self {
Self(Inner::All)
}
#[must_use]
pub fn idempotent() -> Self {
Self(Inner::Idempotent)
}
#[cfg_attr(test, mutants::skip)] #[must_use]
pub fn safe_only() -> Self {
Self(Inner::SafeOnly)
}
pub(super) fn try_clone(
&self,
request: &mut HttpRequest,
attempt: Attempt,
previous_recovery: Option<&RecoveryInfo>,
) -> Option<HttpRequest> {
let mut result = if self.can_clone(request.method()) {
request.try_clone()
} else {
None
};
attach_attempt(result.as_mut().unwrap_or(request), attempt);
if !update_request_uri(result.as_mut().unwrap_or(request), attempt, previous_recovery) {
return None;
}
result
}
fn can_clone(&self, method: &Method) -> bool {
match self.0 {
Inner::All => true,
Inner::Idempotent => method.is_idempotent(),
Inner::SafeOnly => method.is_safe(),
}
}
}
#[must_use]
fn update_request_uri(request: &mut HttpRequest, attempt: Attempt, previous_recovery: Option<&RecoveryInfo>) -> bool {
let router = match request.extensions().get::<Router>() {
Some(router) if router.has_alternatives() && !attempt.is_first() => router.clone(),
_ => return true,
};
let mut context = RouterContext::new().with_attempt(attempt.index(), attempt.is_last());
if let Some(previous_recovery) = previous_recovery {
context = context.with_previous_recovery(previous_recovery.clone());
}
router.resolve_request_uri(context, request).is_ok()
}
#[derive(Debug, Default)]
enum Inner {
#[default]
SafeOnly,
Idempotent,
All,
}
fn attach_attempt(request: &mut HttpRequest, attempt: Attempt) {
request.extensions_mut().insert(attempt);
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use http_extensions::HttpRequestBuilder;
use http_extensions::routing::BaseUriConflict;
use templated_uri::BaseUri;
use super::*;
#[test]
fn test_default_is_safe_only() {
let clone = HttpClone::default();
assert!(clone.can_clone(&Method::GET));
assert!(clone.can_clone(&Method::HEAD));
assert!(clone.can_clone(&Method::OPTIONS));
assert!(!clone.can_clone(&Method::POST));
assert!(!clone.can_clone(&Method::PUT));
assert!(!clone.can_clone(&Method::DELETE));
assert!(!clone.can_clone(&Method::PATCH));
}
#[test]
fn test_safe_only_strategy() {
let clone = HttpClone::safe_only();
assert!(clone.can_clone(&Method::GET));
assert!(clone.can_clone(&Method::HEAD));
assert!(clone.can_clone(&Method::OPTIONS));
assert!(!clone.can_clone(&Method::PUT));
assert!(!clone.can_clone(&Method::DELETE));
assert!(!clone.can_clone(&Method::POST));
assert!(!clone.can_clone(&Method::PATCH));
}
#[test]
fn test_idempotent_strategy() {
let clone = HttpClone::idempotent();
assert!(clone.can_clone(&Method::GET));
assert!(clone.can_clone(&Method::HEAD));
assert!(clone.can_clone(&Method::PUT));
assert!(clone.can_clone(&Method::DELETE));
assert!(clone.can_clone(&Method::OPTIONS));
assert!(!clone.can_clone(&Method::POST));
assert!(!clone.can_clone(&Method::PATCH));
}
#[test]
fn test_all_strategy() {
let clone = HttpClone::all();
assert!(clone.can_clone(&Method::GET));
assert!(clone.can_clone(&Method::HEAD));
assert!(clone.can_clone(&Method::POST));
assert!(clone.can_clone(&Method::PUT));
assert!(clone.can_clone(&Method::DELETE));
assert!(clone.can_clone(&Method::PATCH));
assert!(clone.can_clone(&Method::OPTIONS));
assert!(clone.can_clone(&Method::CONNECT));
assert!(clone.can_clone(&Method::TRACE));
}
#[test]
fn test_custom_method() {
let clone_safe = HttpClone::safe_only();
let clone_idempotent = HttpClone::idempotent();
let clone_all = HttpClone::all();
let custom_method = Method::from_bytes(b"CUSTOM").unwrap();
assert!(!clone_safe.can_clone(&custom_method));
assert!(!clone_idempotent.can_clone(&custom_method));
assert!(clone_all.can_clone(&custom_method));
}
#[test]
fn try_clone_attaches_attempt_to_cloned_request() {
let clone = HttpClone::all();
let attempt = Attempt::new(3, false);
let mut request = HttpRequestBuilder::new_fake()
.method(Method::GET)
.uri("https://example.com")
.build()
.unwrap();
let cloned = clone.try_clone(&mut request, attempt, None);
let cloned = cloned.expect("cloneable request should produce Some");
let attached = cloned
.extensions()
.get::<Attempt>()
.expect("attempt should be attached to the cloned request");
assert_eq!(attached.index(), 3);
assert!(!attached.is_last());
}
#[test]
fn try_clone_returns_none_when_routing_fails() {
let router =
Router::custom(|_| Some(BaseUri::from_static("https://routed.example.com")), true).conflict_policy(BaseUriConflict::Fail);
let mut request = HttpRequestBuilder::new_fake()
.method(Method::GET)
.uri("https://existing.example.com/items")
.extension(router)
.build()
.unwrap();
let clone = HttpClone::all();
let attempt = Attempt::new(1, false);
let result = clone.try_clone(&mut request, attempt, None);
assert!(result.is_none(), "failed routing should drop the clone");
}
#[test]
fn try_clone_attaches_attempt_to_original_when_clone_is_disallowed() {
let clone = HttpClone::safe_only();
let attempt = Attempt::new(1, true);
let mut request = HttpRequestBuilder::new_fake()
.method(Method::POST)
.uri("https://example.com")
.build()
.unwrap();
let result = clone.try_clone(&mut request, attempt, None);
assert!(result.is_none(), "unsafe method should not be cloned");
let attached = request
.extensions()
.get::<Attempt>()
.expect("attempt should be attached to the original request");
assert_eq!(attached.index(), 1);
assert!(attached.is_last());
}
}