Skip to main content

rama_http/layer/follow_redirect/
mod.rs

1//! Middleware for following redirections.
2//!
3//! # Overview
4//!
5//! The [`FollowRedirect`] middleware retries requests with the inner [`Service`] to follow HTTP
6//! redirections.
7//!
8//! The middleware tries to clone the original [`Request`] when making a redirected request.
9//! The request body cannot always be cloned. When the original body is
10//! known to be empty by [`StreamingBody::size_hint`], the middleware uses the `Default`
11//! implementation of the body type to create a new request body. If you know that the body can be
12//! cloned in some way, you can tell the middleware to clone it by configuring a [`policy`].
13//!
14//! By default every attempt — the original request included — runs on its own
15//! [`fork`][Request::fork_extensions_in_place] of the caller's request [`Extensions`]: each hop
16//! reads everything the caller inserted, while what it (or any inner layer) inserts stays isolated
17//! from the caller and from every other hop. Isolation is structural, not deep: an inherited value
18//! is shared by handle, so interior mutation through it (an atomic, a lock) stays visible to the
19//! caller and to every other hop. Only the entries a hop inserts itself are private to it.
20//!
21//! Hop 1 pays for that fork too, redirect or not — its inserts have already happened by the time a
22//! `Location` arrives, so the isolation cannot be deferred until one is seen. It is one
23//! [`Extensions::fork`] (tens of nanoseconds, and one extra level on an extension miss).
24//!
25//! [`Extensions::fork`]: rama_core::extensions::Extensions::fork
26//!
27//! # Layer placement
28//!
29//! Place [`FollowRedirectLayer`] as early — as far outward — in the stack as your use case allows:
30//! in front of everything whose work depends on the request's target, e.g. proxy or route
31//! selection, DNS overwrites, per-origin credentials or per-host limits.
32//!
33//! Such a layer placed _outside_ this middleware runs exactly once, for the original target, and
34//! every hop then inherits the decision it made for a different host or resource: hop 2 to
35//! `internal.corp` is routed by the proxy hop 1 picked for `public.example`. Placed _inside_, it is
36//! consulted per hop and decides on that hop's real target.
37//!
38//! The same holds for the extensions themselves: an inner layer's inserts cannot leak between hops,
39//! but a layer outside this middleware inserts into the caller's request store, which every hop —
40//! cross-origin ones included — inherits and can read. rama's [`Extensions`] are append-only, so no
41//! [`policy`] (including [`FilterCredentials`]) can strip them afterwards. Keeping origin-scoped
42//! state away from a redirect target therefore takes both halves: inserted _inside_ this middleware
43//! _and_ derived from the hop's own target. Inside alone only makes the decision re-decidable — a
44//! layer that inserts the same value for every target (e.g. `ProxyAddressLayer`, which stamps
45//! one configured proxy) hands that value to each hop regardless of where it points. When the value
46//! is inserted by the caller, scope the value itself so it cannot authorize or configure an
47//! unrelated redirect target.
48//!
49//! The `rama` CLI keeps [`AddAuthorizationLayer`] outside on purpose: it sets a header rather than
50//! an extension, so [`FilterCredentials`] _can_ strip it on a cross-origin hop.
51//!
52//! Consulting a per-request decider on every hop does mean an attacker-controlled `Location` chain
53//! costs N decisions instead of 1, bounded by the redirect [`policy`]'s own limit. That is the
54//! correct trade: the alternative is routing hop N by hop 1's answer.
55//!
56//! Paired with [`retry`](crate::layer::retry) — which follows the same rules per attempt — keep this
57//! middleware outermost, so a retry replays one hop instead of the entire redirect chain.
58//!
59//! [`AddAuthorizationLayer`]: crate::layer::auth::AddAuthorizationLayer
60//! [`Extensions`]: rama_core::extensions::Extensions
61//! [`FilterCredentials`]: policy::FilterCredentials
62//!
63//! # Examples
64//!
65//! ## Basic usage
66//!
67//! ```
68//! use rama_core::service::service_fn;
69//! use rama_core::{extensions::ExtensionsRef, Service, Layer};
70//! use rama_http::{Body, Request, Response, StatusCode, header};
71//! use rama_http::layer::follow_redirect::{FollowRedirectLayer, RequestUri};
72//!
73//! # #[tokio::main]
74//! # async fn main() -> Result<(), std::convert::Infallible> {
75//! # let http_client = service_fn(async |req: Request| {
76//! #     let dest = "https://www.rust-lang.org/";
77//! #     let mut res = Response::builder();
78//! #     if req.uri().as_str() != dest {
79//! #         res = res
80//! #             .status(StatusCode::MOVED_PERMANENTLY)
81//! #             .header(header::LOCATION, dest);
82//! #     }
83//! #     Ok::<_, std::convert::Infallible>(res.body(Body::empty()).unwrap())
84//! # });
85//! let mut client = FollowRedirectLayer::new().into_layer(http_client);
86//!
87//! let request = Request::builder()
88//!     .uri("https://rust-lang.org/")
89//!     .body(Body::empty())
90//!     .unwrap();
91//!
92//! let response = client.serve(request).await?;
93//! // Get the final request URI.
94//! assert_eq!(response.extensions().get_ref::<RequestUri>().unwrap().0.as_str(), "https://www.rust-lang.org/");
95//! # Ok(())
96//! # }
97//! ```
98//!
99//! ## Customizing the `Policy`
100//!
101//! You can use a [`Policy`] value to customize how the middleware handles redirections.
102//!
103//! ```
104//! # #![allow(unused)]
105//!
106//! # use std::convert::Infallible;
107//! use rama_core::service::service_fn;
108//! use rama_core::layer::MapErrLayer;
109//! use rama_core::{Service, Layer};
110//! use rama_http::{Body, Request, Response};
111//! use rama_http::layer::follow_redirect::{
112//!     policy::{self, PolicyExt},
113//!     FollowRedirectLayer,
114//! };
115//! use rama_core::error::BoxError;
116//!
117//! #[derive(Debug)]
118//! enum MyError {
119//!     TooManyRedirects,
120//!     Other(BoxError),
121//! }
122//!
123//! impl MyError {
124//!     fn from_std(err: impl std::error::Error + Send + Sync + 'static) -> Self {
125//!         Self::Other(BoxError::from(err))
126//!     }
127//!
128//! }
129//!
130//! # #[tokio::main]
131//! # async fn main() -> Result<(), MyError> {
132//! # let http_client = service_fn(async |_: Request| Ok::<_, Infallible>(Response::new(Body::empty())));
133//! let policy = policy::Limited::new(10) // Set the maximum number of redirections to 10.
134//!     // Return an error when the limit was reached.
135//!     .or::<_, (), _>(policy::redirect_fn(|_| Err(MyError::TooManyRedirects)))
136//!     // Do not follow cross-origin redirections, and return the redirection responses as-is.
137//!     .and::<_, (), _>(policy::SameOrigin::new());
138//!
139//! let client = (
140//!     FollowRedirectLayer::with_policy(policy),
141//!     MapErrLayer::new(MyError::from_std),
142//! ).into_layer(http_client);
143//!
144//! // ...
145//! _ = client.serve(Request::default()).await?;
146//! # Ok(())
147//! # }
148//! ```
149
150pub mod policy;
151
152use crate::{Method, Request, Response, StatusCode, StreamingBody, header::LOCATION};
153use iri_string::types::{UriAbsoluteString, UriReferenceStr};
154use rama_core::{
155    Layer, Service,
156    extensions::{Extension, ExtensionsRef},
157};
158use rama_http_types::{
159    HeaderMap,
160    header::{CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TYPE, TRANSFER_ENCODING},
161};
162use rama_net::uri::Uri;
163use rama_utils::macros::define_inner_service_accessors;
164use std::fmt;
165
166use self::policy::{Action, Attempt, Policy, Standard};
167
168/// [`Layer`] for retrying requests with a [`Service`] to follow redirection responses.
169///
170/// See the [module docs](self) for more details.
171#[derive(Clone)]
172pub struct FollowRedirectLayer<P = Standard> {
173    policy: P,
174}
175
176impl FollowRedirectLayer {
177    /// Create a new [`FollowRedirectLayer`] with a [`Standard`] redirection policy.
178    #[must_use]
179    pub fn new() -> Self {
180        Self::default()
181    }
182}
183
184impl Default for FollowRedirectLayer {
185    fn default() -> Self {
186        Self::with_policy(Standard::default())
187    }
188}
189
190impl<P: fmt::Debug> fmt::Debug for FollowRedirectLayer<P> {
191    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192        f.debug_struct("FollowRedirectLayer")
193            .field("policy", &self.policy)
194            .finish()
195    }
196}
197
198impl<P> FollowRedirectLayer<P> {
199    /// Create a new [`FollowRedirectLayer`] with the given redirection [`Policy`].
200    pub const fn with_policy(policy: P) -> Self {
201        Self { policy }
202    }
203}
204
205impl<S, P> Layer<S> for FollowRedirectLayer<P>
206where
207    S: Clone,
208    P: Clone,
209{
210    type Service = FollowRedirect<S, P>;
211
212    fn layer(&self, inner: S) -> Self::Service {
213        FollowRedirect {
214            inner,
215            policy: self.policy.clone(),
216        }
217    }
218
219    fn into_layer(self, inner: S) -> Self::Service {
220        FollowRedirect {
221            inner,
222            policy: self.policy,
223        }
224    }
225}
226
227/// Middleware that retries requests with a [`Service`] to follow redirection responses.
228///
229/// See the [module docs](self) for more details.
230#[derive(Debug, Clone)]
231pub struct FollowRedirect<S, P = Standard> {
232    inner: S,
233    policy: P,
234}
235
236impl<S> FollowRedirect<S> {
237    /// Create a new [`FollowRedirect`] with a [`Standard`] redirection policy.
238    pub fn new(inner: S) -> Self {
239        Self::with_policy(inner, Standard::default())
240    }
241}
242
243impl<S, P> FollowRedirect<S, P> {
244    /// Create a new [`FollowRedirect`] with the given redirection [`Policy`].
245    pub const fn with_policy(inner: S, policy: P) -> Self {
246        Self { inner, policy }
247    }
248
249    define_inner_service_accessors!();
250}
251
252impl<ReqBody, ResBody, S, P> Service<Request<ReqBody>> for FollowRedirect<S, P>
253where
254    S: Service<Request<ReqBody>, Output = Response<ResBody>>,
255    ReqBody: StreamingBody + Default + Send + 'static,
256    ResBody: Send + 'static,
257    P: Policy<ReqBody, S::Error> + Clone,
258{
259    type Output = Response<ResBody>;
260    type Error = S::Error;
261
262    fn serve(
263        &self,
264        mut req: Request<ReqBody>,
265    ) -> impl Future<Output = Result<Self::Output, Self::Error>> {
266        let mut method = req.method().clone();
267        let mut uri = req.uri().clone();
268        let version = req.version();
269
270        let mut policy = self.policy.clone();
271
272        let mut body = BodyRepr::None;
273        body.try_clone_from(&mut policy, req.body());
274
275        // Hand every attempt — this first one included — its own child store, so a per-hop insert
276        // can never leak into a later hop or back to the caller.
277        let caller_extensions = req.fork_extensions_in_place();
278
279        policy.on_request(&mut req);
280        // Start the redirect template from what the policy actually sent on
281        // hop 1, just as later hops carry their post-policy headers forward.
282        let mut headers = req.headers().clone();
283
284        let service = &self.inner;
285
286        async move {
287            loop {
288                let res = service.serve(req).await?;
289                res.extensions().insert(RequestUri(uri.clone()));
290
291                let previous_method = method.clone();
292                let drop_payload_headers = |headers: &mut HeaderMap| {
293                    for header in &[
294                        CONTENT_TYPE,
295                        CONTENT_LENGTH,
296                        CONTENT_ENCODING,
297                        TRANSFER_ENCODING,
298                    ] {
299                        headers.remove(header);
300                    }
301                };
302
303                match res.status() {
304                    StatusCode::MOVED_PERMANENTLY | StatusCode::FOUND => {
305                        // User agents MAY change the request method from POST to GET
306                        // (RFC 7231 section 6.4.2. and 6.4.3.).
307                        if method == Method::POST {
308                            method = Method::GET;
309                            body = BodyRepr::Empty;
310                            drop_payload_headers(&mut headers);
311                        }
312                    }
313                    StatusCode::SEE_OTHER => {
314                        // A user agent can perform a GET or HEAD request (RFC 7231 section 6.4.4.).
315                        if method != Method::HEAD {
316                            method = Method::GET;
317                        }
318                        body = BodyRepr::Empty;
319                        drop_payload_headers(&mut headers);
320                    }
321                    StatusCode::TEMPORARY_REDIRECT | StatusCode::PERMANENT_REDIRECT => {}
322                    _ => return Ok(res),
323                };
324
325                let Some(taken_body) = body.take() else {
326                    return Ok(res);
327                };
328
329                let location = res
330                    .headers()
331                    .get(&LOCATION)
332                    .and_then(|loc| resolve_uri(std::str::from_utf8(loc.as_bytes()).ok()?, &uri));
333                let Some(location) = location else {
334                    return Ok(res);
335                };
336
337                let attempt = Attempt {
338                    status: res.status(),
339                    method: &method,
340                    location: &location,
341                    previous_method: &previous_method,
342                    previous: &uri,
343                };
344                match policy.redirect(&attempt)? {
345                    Action::Follow => {
346                        uri = location;
347                        body.try_clone_from(&mut policy, &taken_body);
348
349                        req = Request::new(taken_body);
350                        *req.uri_mut() = uri.clone();
351                        *req.method_mut() = method.clone();
352                        *req.version_mut() = version;
353                        *req.headers_mut() = headers.clone();
354                        req.set_extensions(caller_extensions.fork());
355                        policy.on_request(&mut req);
356                        // Carry the filtered headers forward so anything dropped on this hop
357                        // stays dropped on the next one (e.g. credentials after a cross-origin
358                        // hop must not resurrect on a later same-origin hop).
359                        headers = req.headers().clone();
360                    }
361                    Action::Stop => return Ok(res),
362                }
363            }
364        }
365    }
366}
367
368/// Response [`Extensions`] value that represents the effective request URI of
369/// a response returned by a [`FollowRedirect`] middleware.
370///
371/// The value differs from the original request's effective URI if the middleware has followed
372/// redirections.
373///
374/// [`Extensions`]: rama_core::extensions::Extensions
375#[derive(Debug, Clone, Extension)]
376#[extension(tags(http))]
377pub struct RequestUri(pub Uri);
378
379#[derive(Debug)]
380enum BodyRepr<B> {
381    Some(B),
382    Empty,
383    None,
384}
385
386impl<B> BodyRepr<B>
387where
388    B: StreamingBody + Default,
389{
390    fn take(&mut self) -> Option<B> {
391        match std::mem::replace(self, Self::None) {
392            Self::Some(body) => Some(body),
393            Self::Empty => {
394                *self = Self::Empty;
395                Some(B::default())
396            }
397            Self::None => None,
398        }
399    }
400
401    fn try_clone_from<P, E>(&mut self, policy: &mut P, body: &B)
402    where
403        P: Policy<B, E>,
404    {
405        match self {
406            Self::Some(_) | Self::Empty => {}
407            Self::None => {
408                if let Some(body) = clone_body(policy, body) {
409                    *self = Self::Some(body);
410                }
411            }
412        }
413    }
414}
415
416fn clone_body<P, B, E>(policy: &mut P, body: &B) -> Option<B>
417where
418    P: Policy<B, E>,
419    B: StreamingBody + Default,
420{
421    if body.size_hint().exact() == Some(0) {
422        Some(B::default())
423    } else {
424        policy.clone_body(body)
425    }
426}
427
428/// Try to resolve a URI reference `relative` against a base URI `base`.
429fn resolve_uri(relative: &str, base: &Uri) -> Option<Uri> {
430    let relative = UriReferenceStr::new(relative).ok()?;
431    let base = UriAbsoluteString::try_from(base.to_string()).ok()?;
432    let uri = relative.resolve_against(&base).to_string();
433    Uri::try_from(uri).ok()
434}
435
436/* // ^TODO replace w/ something similar to
437let base_url = Url::parse(&base.to_string()).ok()?;
438let resolved = base_url.join(relative).ok()?;
439Uri::try_from(String::from(resolved)).ok()
440*/
441
442#[cfg(test)]
443mod tests {
444    use super::{policy::*, *};
445    use crate::{Body, header::LOCATION};
446    use parking_lot::Mutex;
447    use rama_core::Layer;
448    use rama_core::extensions::ExtensionsRef;
449    use rama_core::service::service_fn;
450    use std::{convert::Infallible, sync::Arc};
451
452    #[test]
453    fn layer_debug_includes_policy() {
454        assert_eq!(
455            format!("{:?}", FollowRedirectLayer::with_policy(Action::Follow)),
456            "FollowRedirectLayer { policy: Follow }"
457        );
458    }
459
460    #[test]
461    fn body_representation_preserves_clone_refusal_and_empty_state() {
462        let mut none = BodyRepr::<Body>::None;
463        assert!(none.take().is_none());
464
465        let mut empty = BodyRepr::<Body>::Empty;
466        assert!(empty.take().is_some());
467        assert!(empty.take().is_some());
468
469        let mut policy = Action::Follow;
470        assert!(
471            clone_body::<_, _, Infallible>(&mut policy, &Body::from("not cloneable")).is_none()
472        );
473        assert!(clone_body::<_, _, Infallible>(&mut policy, &Body::empty()).is_some());
474    }
475
476    #[tokio::test]
477    async fn follows() {
478        let svc = FollowRedirectLayer::with_policy(Action::Follow).into_layer(service_fn(handle));
479        let req = Request::builder()
480            .uri("http://example.com/42")
481            .body(Body::empty())
482            .unwrap();
483        let res = svc.serve(req).await.unwrap();
484        assert_eq!(*res.body(), 0);
485        assert_eq!(
486            res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
487            "http://example.com/0"
488        );
489    }
490
491    #[tokio::test]
492    async fn stops() {
493        let svc = FollowRedirectLayer::with_policy(Action::Stop).into_layer(service_fn(handle));
494        let req = Request::builder()
495            .uri("http://example.com/42")
496            .body(Body::empty())
497            .unwrap();
498        let res = svc.serve(req).await.unwrap();
499        assert_eq!(*res.body(), 42);
500        assert_eq!(
501            res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
502            "http://example.com/42"
503        );
504    }
505
506    #[tokio::test]
507    async fn limited() {
508        let svc = FollowRedirectLayer::with_policy(Limited::new(10)).into_layer(service_fn(handle));
509        let req = Request::builder()
510            .uri("http://example.com/42")
511            .body(Body::empty())
512            .unwrap();
513        let res = svc.serve(req).await.unwrap();
514        assert_eq!(*res.body(), 42 - 10);
515        assert_eq!(
516            res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
517            "http://example.com/32"
518        );
519    }
520
521    /// A server with an endpoint `/{n}` which redirects to `/{n-1}` unless `n` equals zero,
522    /// returning `n` as the response body.
523    async fn handle<B>(req: Request<B>) -> Result<Response<u64>, Infallible> {
524        let n: u64 = req
525            .uri()
526            .first_path_segment()
527            .and_then(|segment| segment.as_encoded_str().parse().ok())
528            .unwrap();
529        let mut res = Response::builder();
530        if n > 0 {
531            res = res
532                .status(StatusCode::MOVED_PERMANENTLY)
533                .header(LOCATION, format!("/{}", n - 1));
534        }
535        Ok::<_, Infallible>(res.body(n).unwrap())
536    }
537
538    #[derive(Clone, Debug, PartialEq, rama_core::extensions::Extension)]
539    struct Marker(u32);
540
541    /// Like [`handle`] but also copies a `Marker` request extension onto the response, so a test
542    /// can observe whether it reached the (final, redirected) request.
543    async fn handle_marker<B>(req: Request<B>) -> Result<Response<u64>, Infallible> {
544        let n: u64 = req
545            .uri()
546            .first_path_segment()
547            .and_then(|segment| segment.as_encoded_str().parse().ok())
548            .unwrap();
549        let mut res = Response::builder();
550        if n > 0 {
551            res = res
552                .status(StatusCode::MOVED_PERMANENTLY)
553                .header(LOCATION, format!("/{}", n - 1));
554        }
555        let res = res.body(n).unwrap();
556        if let Some(marker) = req.extensions().get_ref::<Marker>() {
557            res.extensions().insert(marker.clone());
558        }
559        Ok::<_, Infallible>(res)
560    }
561
562    #[tokio::test]
563    async fn final_hop_reads_the_callers_extensions() {
564        let svc = FollowRedirectLayer::new().into_layer(service_fn(handle_marker));
565        let req = Request::builder()
566            .uri("http://example.com/3")
567            .body(Body::empty())
568            .unwrap();
569        req.extensions().insert(Marker(7));
570        let res = svc.serve(req).await.unwrap();
571        // A fork reads through to its parent, so every hop still sees the caller's extensions.
572        assert_eq!(res.extensions().get_ref::<Marker>(), Some(&Marker(7)));
573    }
574
575    #[tokio::test]
576    async fn fork_reads_the_callers_extensions_on_every_hop() {
577        let hops = Arc::new(Mutex::new(Vec::new()));
578        let svc = FollowRedirectLayer::new().into_layer(service_fn({
579            let hops = hops.clone();
580            move |req: Request<Body>| {
581                hops.lock()
582                    .push(req.extensions().get_ref::<Marker>().cloned());
583                handle(req)
584            }
585        }));
586        let req = Request::builder()
587            .uri("http://example.com/2")
588            .body(Body::empty())
589            .unwrap();
590        req.extensions().insert(Marker(7));
591
592        svc.serve(req).await.unwrap();
593        assert_eq!(
594            hops.lock().as_slice(),
595            [Some(Marker(7)), Some(Marker(7)), Some(Marker(7))],
596        );
597    }
598
599    /// A policy that stamps a fresh `Marker` in `on_request` unless the request already carries one
600    /// — the shape of an idempotent per-request preparation step.
601    #[derive(Debug, Clone, Default)]
602    struct StampingPolicy(u32);
603
604    impl<B, E> Policy<B, E> for StampingPolicy {
605        fn redirect(&mut self, _: &Attempt<'_>) -> Result<Action, E> {
606            Ok(Action::Follow)
607        }
608
609        fn on_request(&mut self, req: &mut Request<B>) {
610            if !req.extensions().contains::<Marker>() {
611                req.extensions().insert(Marker(self.0));
612                self.0 += 1;
613            }
614        }
615    }
616
617    #[tokio::test]
618    async fn policy_on_request_inserts_stay_per_hop() {
619        // `on_request` runs after the hop's store is forked, so its inserts are per-hop too: hop 2
620        // gets its own stamp rather than inheriting hop 1's, and the caller's store never sees one.
621        let hops = Arc::new(Mutex::new(Vec::new()));
622        let svc =
623            FollowRedirectLayer::with_policy(StampingPolicy::default()).into_layer(service_fn({
624                let hops = hops.clone();
625                move |req: Request<Body>| {
626                    hops.lock()
627                        .push(req.extensions().get_ref::<Marker>().cloned());
628                    handle(req)
629                }
630            }));
631        let req = Request::builder()
632            .uri("http://example.com/2")
633            .body(Body::empty())
634            .unwrap();
635        let caller_extensions = req.extensions().clone();
636
637        svc.serve(req).await.unwrap();
638        assert_eq!(
639            hops.lock().as_slice(),
640            [Some(Marker(0)), Some(Marker(1)), Some(Marker(2))],
641        );
642        assert!(!caller_extensions.contains::<Marker>());
643    }
644
645    #[derive(Debug, Clone, Default)]
646    struct RemoveHeaderOnce(bool);
647
648    impl<B, E> Policy<B, E> for RemoveHeaderOnce {
649        fn redirect(&mut self, _: &Attempt<'_>) -> Result<Action, E> {
650            Ok(Action::Follow)
651        }
652
653        fn on_request(&mut self, req: &mut Request<B>) {
654            if !self.0 {
655                req.headers_mut().remove("x-remove-on-first-hop");
656                self.0 = true;
657            }
658        }
659    }
660
661    #[tokio::test]
662    async fn first_hop_policy_header_changes_are_carried_forward() {
663        let seen = Arc::new(Mutex::new(Vec::new()));
664        let svc =
665            FollowRedirectLayer::with_policy(RemoveHeaderOnce::default()).into_layer(service_fn({
666                let seen = seen.clone();
667                move |req: Request<Body>| {
668                    seen.lock()
669                        .push(req.headers().contains_key("x-remove-on-first-hop"));
670                    handle(req)
671                }
672            }));
673        let req = Request::builder()
674            .uri("http://example.com/1")
675            .header("x-remove-on-first-hop", "yes")
676            .body(Body::empty())
677            .unwrap();
678
679        svc.serve(req).await.unwrap();
680        assert_eq!(seen.lock().as_slice(), [false, false]);
681    }
682
683    /// Drives a cross-origin redirect chain and echoes, via `x-saw-cookie`, whether the incoming
684    /// request still carried a `Cookie`:
685    /// `a.example.com` → `b.example.com/second` (cross-origin) → `b.example.com/final` (same-origin).
686    async fn handle_cookie_chain<B>(req: Request<B>) -> Result<Response<u64>, Infallible> {
687        let host = req.uri().host_str();
688        let path = req.uri().path_ref_or_root();
689        let location = if host.as_deref() == Some("a.example.com") {
690            Some("http://b.example.com/second")
691        } else if host.as_deref() == Some("b.example.com") && path == "/second" {
692            Some("http://b.example.com/final")
693        } else {
694            None
695        };
696        let mut res = Response::builder();
697        if let Some(location) = location {
698            res = res
699                .status(StatusCode::MOVED_PERMANENTLY)
700                .header(LOCATION, location);
701        }
702        let mut res = res.body(0u64).unwrap();
703        if req.headers().contains_key(crate::header::COOKIE) {
704            res.headers_mut()
705                .insert("x-saw-cookie", crate::HeaderValue::from_static("1"));
706        }
707        Ok::<_, Infallible>(res)
708    }
709
710    #[tokio::test]
711    async fn credentials_do_not_resurrect_after_cross_origin() {
712        // Regression for the cumulative-filtering half of tower-http #706: the default Standard
713        // policy strips Cookie on the cross-origin a→b hop; it must NOT reappear on the later
714        // same-origin b→b hop just because the original header snapshot is replayed.
715        let svc = FollowRedirectLayer::default().into_layer(service_fn(handle_cookie_chain));
716        let req = Request::builder()
717            .uri("http://a.example.com/")
718            .header(crate::header::COOKIE, "session=secret")
719            .body(Body::empty())
720            .unwrap();
721        let res = svc.serve(req).await.unwrap();
722        assert!(
723            !res.headers().contains_key("x-saw-cookie"),
724            "Cookie resurrected on a same-origin hop after being dropped cross-origin",
725        );
726        assert_eq!(
727            res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
728            "http://b.example.com/final"
729        );
730    }
731
732    /// A layer that derives a `Route` from the request target, unless the current attempt already
733    /// carries one — the shape of every target-dependent decider (proxy/route selection, DNS
734    /// overwrite, ...). It records what each attempt ended up with.
735    #[derive(Debug, Clone)]
736    struct RouteDecider<S> {
737        inner: S,
738        seen: Arc<Mutex<Vec<String>>>,
739    }
740
741    #[derive(Clone, Debug, PartialEq, rama_core::extensions::Extension)]
742    struct Route(String);
743
744    impl<S, B> Service<Request<B>> for RouteDecider<S>
745    where
746        S: Service<Request<B>>,
747        B: Send + 'static,
748    {
749        type Output = S::Output;
750        type Error = S::Error;
751
752        async fn serve(&self, req: Request<B>) -> Result<Self::Output, Self::Error> {
753            if req.extensions().get_ref::<Route>().is_none() {
754                let host = req.uri().host_str().unwrap_or_default().into_owned();
755                req.extensions().insert(Route(host));
756            }
757            self.seen
758                .lock()
759                .push(req.extensions().get_ref::<Route>().unwrap().0.clone());
760            self.inner.serve(req).await
761        }
762    }
763
764    #[tokio::test]
765    async fn inner_per_hop_decision_does_not_leak_into_the_next_hop() {
766        // Regression: an inner decider must be consulted with *this* hop's target, never with the
767        // route hop 1 picked for a different host.
768        let seen = Arc::new(Mutex::new(Vec::new()));
769        let svc = FollowRedirectLayer::new().into_layer(RouteDecider {
770            inner: service_fn(handle_cookie_chain),
771            seen: seen.clone(),
772        });
773        let req = Request::builder()
774            .uri("http://a.example.com/")
775            .body(Body::empty())
776            .unwrap();
777
778        svc.serve(req).await.unwrap();
779        assert_eq!(
780            seen.lock().as_slice(),
781            ["a.example.com", "b.example.com", "b.example.com"],
782        );
783    }
784
785    #[tokio::test]
786    async fn outer_per_hop_decision_is_inherited_by_every_hop() {
787        // The flip side of the above, and why placement matters: a decider *outside* the middleware
788        // runs once, and its verdict for the original target is what every hop is routed by.
789        let seen = Arc::new(Mutex::new(Vec::new()));
790        let svc = RouteDecider {
791            inner: FollowRedirectLayer::new().into_layer(service_fn({
792                let seen = seen.clone();
793                move |req: Request<Body>| {
794                    seen.lock()
795                        .push(req.extensions().get_ref::<Route>().unwrap().0.clone());
796                    handle_cookie_chain(req)
797                }
798            })),
799            seen: Arc::new(Mutex::new(Vec::new())),
800        };
801        let req = Request::builder()
802            .uri("http://a.example.com/")
803            .body(Body::empty())
804            .unwrap();
805
806        svc.serve(req).await.unwrap();
807        assert_eq!(
808            seen.lock().as_slice(),
809            ["a.example.com", "a.example.com", "a.example.com"],
810        );
811    }
812
813    #[tokio::test]
814    async fn hop_inserts_never_reach_the_callers_request_store() {
815        let svc = FollowRedirectLayer::new().into_layer(RouteDecider {
816            inner: service_fn(handle_cookie_chain),
817            seen: Arc::new(Mutex::new(Vec::new())),
818        });
819        let req = Request::builder()
820            .uri("http://a.example.com/")
821            .body(Body::empty())
822            .unwrap();
823        let caller_extensions = req.extensions().clone();
824
825        svc.serve(req).await.unwrap();
826        assert!(
827            !caller_extensions.contains::<Route>(),
828            "a hop's insert leaked back into the caller's request extensions",
829        );
830    }
831
832    #[tokio::test]
833    async fn response_still_exposes_the_final_hops_extensions() {
834        // Isolating the request store does not hide per-hop metadata from the caller: a response
835        // forks from the request that produced it (as the h1/h2 client stacks do), so the final
836        // hop's chain — and the caller's own extensions — remain readable through the response.
837        let svc = FollowRedirectLayer::new().into_layer(RouteDecider {
838            inner: service_fn(async |req: Request<Body>| {
839                let request_extensions = req.extensions().clone();
840                let mut res = handle_cookie_chain(req).await?;
841                res.set_extensions(request_extensions.fork());
842                Ok::<_, Infallible>(res)
843            }),
844            seen: Arc::new(Mutex::new(Vec::new())),
845        });
846        let req = Request::builder()
847            .uri("http://a.example.com/")
848            .body(Body::empty())
849            .unwrap();
850        req.extensions().insert(Marker(7));
851
852        let res = svc.serve(req).await.unwrap();
853        assert_eq!(
854            res.extensions().get_ref::<Route>(),
855            Some(&Route("b.example.com".to_owned())),
856        );
857        assert_eq!(res.extensions().get_ref::<Marker>(), Some(&Marker(7)));
858    }
859
860    #[tokio::test]
861    async fn test_301_redirects() {
862        let policy = policy::redirect_fn(|attempt| -> Result<_, Infallible> {
863            if attempt.previous_method() == Method::POST && attempt.method() == Method::GET {
864                Ok(Action::Stop)
865            } else {
866                Ok(Action::Follow)
867            }
868        });
869        let svc = FollowRedirectLayer::with_policy(policy).into_layer(service_fn(redirections));
870
871        // A POST request with a 301 redirection should turn into a GET
872        // request, and the policy should stop the redirection.
873        {
874            let req = Request::builder()
875                .method(Method::POST)
876                .uri("http://example.com/301")
877                .body(Body::empty())
878                .unwrap();
879            let res = svc.clone().serve(req).await.unwrap();
880            assert_eq!(*res.body(), "/target/301");
881            assert_eq!(
882                res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
883                "http://example.com/301"
884            );
885        }
886
887        // A GET request with a 301 redirection should remain a GET
888        // request, and the policy should allow the redirection.
889        {
890            let req = Request::builder()
891                .method(Method::GET)
892                .uri("http://example.com/301")
893                .body(Body::empty())
894                .unwrap();
895            let res = svc.clone().serve(req).await.unwrap();
896            assert_eq!(*res.body(), "/target/301/final");
897            assert_eq!(
898                res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
899                "http://example.com/target/301"
900            );
901        }
902    }
903
904    #[tokio::test]
905    async fn test_302_redirects() {
906        let policy = policy::redirect_fn(|attempt| -> Result<_, Infallible> {
907            if attempt.previous_method() != attempt.method() {
908                Ok(Action::Stop)
909            } else {
910                Ok(Action::Follow)
911            }
912        });
913        let svc = FollowRedirectLayer::with_policy(policy).into_layer(service_fn(redirections));
914
915        // A POST request with a 302 redirection should turn into a GET
916        // request, and the policy should stop the redirection.
917        {
918            let req = Request::builder()
919                .method(Method::POST)
920                .uri("http://example.com/302")
921                .body(Body::empty())
922                .unwrap();
923            let res = svc.clone().serve(req).await.unwrap();
924            assert_eq!(*res.body(), "/target/302");
925            assert_eq!(
926                res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
927                "http://example.com/302"
928            );
929        }
930
931        // A PUT request with a 302 redirection should remain a PUT
932        // request, and the policy should allow the redirection.
933        {
934            let req = Request::builder()
935                .method(Method::PUT)
936                .uri("http://example.com/302")
937                .body(Body::empty())
938                .unwrap();
939            let res = svc.clone().serve(req).await.unwrap();
940            assert_eq!(*res.body(), "/target/302/final");
941            assert_eq!(
942                res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
943                "http://example.com/target/302"
944            );
945        }
946
947        // A HEAD request with a 302 redirection should remain a HEAD
948        // request, and the policy should allow the redirection.
949        {
950            let req = Request::builder()
951                .method(Method::HEAD)
952                .uri("http://example.com/302")
953                .body(Body::empty())
954                .unwrap();
955            let res = svc.clone().serve(req).await.unwrap();
956            assert_eq!(*res.body(), "/target/302/final");
957            assert_eq!(
958                res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
959                "http://example.com/target/302"
960            );
961        }
962    }
963
964    #[tokio::test]
965    async fn test_303_redirects() {
966        let policy = policy::redirect_fn(|attempt| -> Result<_, Infallible> {
967            if attempt.previous_method() != attempt.method() {
968                Ok(Action::Stop)
969            } else {
970                Ok(Action::Follow)
971            }
972        });
973        let svc = FollowRedirectLayer::with_policy(policy).into_layer(service_fn(redirections));
974
975        // A POST request with a 303 redirection should turn into a GET
976        // request, and the policy should stop the redirection.
977        {
978            let req = Request::builder()
979                .method(Method::POST)
980                .uri("http://example.com/303")
981                .body(Body::empty())
982                .unwrap();
983            let res = svc.clone().serve(req).await.unwrap();
984            assert_eq!(*res.body(), "/target/303");
985            assert_eq!(
986                res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
987                "http://example.com/303"
988            );
989        }
990
991        // A PUT request with a 303 redirection should turn into a GET
992        // request, and the policy should stop the redirection.
993        {
994            let req = Request::builder()
995                .method(Method::PUT)
996                .uri("http://example.com/303")
997                .body(Body::empty())
998                .unwrap();
999            let res = svc.clone().serve(req).await.unwrap();
1000            assert_eq!(*res.body(), "/target/303");
1001            assert_eq!(
1002                res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
1003                "http://example.com/303"
1004            );
1005        }
1006
1007        // A HEAD request with a 303 redirection should remain a HEAD
1008        // request, and the policy should allow the redirection.
1009        {
1010            let req = Request::builder()
1011                .method(Method::HEAD)
1012                .uri("http://example.com/303")
1013                .body(Body::empty())
1014                .unwrap();
1015            let res = svc.clone().serve(req).await.unwrap();
1016            assert_eq!(*res.body(), "/target/303/final");
1017            assert_eq!(
1018                res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
1019                "http://example.com/target/303"
1020            );
1021        }
1022    }
1023
1024    #[tokio::test]
1025    async fn test_307_308_redirects() {
1026        let policy = policy::redirect_fn(|attempt| -> Result<_, Infallible> {
1027            if attempt.previous_method() != Method::POST || attempt.method() != Method::POST {
1028                Ok(Action::Stop)
1029            } else {
1030                Ok(Action::Follow)
1031            }
1032        });
1033        let svc = FollowRedirectLayer::with_policy(policy).into_layer(service_fn(redirections));
1034
1035        // A POST request with a 307 redirection should remain a POST
1036        // request, and the policy should allow the redirection.
1037        {
1038            let req = Request::builder()
1039                .method(Method::POST)
1040                .uri("http://example.com/307")
1041                .body(Body::empty())
1042                .unwrap();
1043            let res = svc.clone().serve(req).await.unwrap();
1044            assert_eq!(*res.body(), "/target/307/final");
1045            assert_eq!(
1046                res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
1047                "http://example.com/target/307"
1048            );
1049        }
1050
1051        // A POST request with a 308 redirection should remain a POST
1052        // request, and the policy should allow the redirection.
1053        {
1054            let req = Request::builder()
1055                .method(Method::POST)
1056                .uri("http://example.com/308")
1057                .body(Body::empty())
1058                .unwrap();
1059            let res = svc.clone().serve(req).await.unwrap();
1060            assert_eq!(*res.body(), "/target/308/final");
1061            assert_eq!(
1062                res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
1063                "http://example.com/target/308"
1064            );
1065        }
1066    }
1067
1068    /// Returns different 3xx redirections based on the request's URI.
1069    async fn redirections<B>(req: Request<B>) -> Result<Response<String>, Infallible> {
1070        let path = req.uri().path_ref_or_root();
1071        let mut res = Response::builder();
1072        let body_str;
1073        res = if path == "/301" {
1074            let case = "/target/301";
1075            body_str = case.to_owned();
1076            res.status(StatusCode::MOVED_PERMANENTLY)
1077                .header(LOCATION, case)
1078        } else if path == "/302" {
1079            let case = "/target/302";
1080            body_str = case.to_owned();
1081            res.status(StatusCode::FOUND).header(LOCATION, case)
1082        } else if path == "/303" {
1083            let case = "/target/303";
1084            body_str = case.to_owned();
1085            res.status(StatusCode::SEE_OTHER).header(LOCATION, case)
1086        } else if path == "/307" {
1087            let case = "/target/307";
1088            body_str = case.to_owned();
1089            res.status(StatusCode::TEMPORARY_REDIRECT)
1090                .header(LOCATION, case)
1091        } else if path == "/308" {
1092            let case = "/target/308";
1093            body_str = case.to_owned();
1094            res.status(StatusCode::PERMANENT_REDIRECT)
1095                .header(LOCATION, case)
1096        } else {
1097            body_str = format!("{path}/final");
1098            res.status(StatusCode::OK)
1099        };
1100        Ok::<_, Infallible>(res.body(body_str).unwrap())
1101    }
1102
1103    // TOOD: adapt + enable once we did Uri rework
1104    // #[tokio::test]
1105    // async fn test_resolve_uri_unicode() {
1106    //     let base = Uri::from_static("https://example.com/api");
1107    //     // Case 1: Unicode in path
1108    //     let relative = "/café";
1109    //     let resolved = resolve_uri(relative, &base);
1110    //     assert!(resolved.is_some(), "Should resolve URI with unicode path");
1111    //     assert_eq!(
1112    //         resolved.unwrap().to_string(),
1113    //         "https://example.com/caf%C3%A9"
1114    //     );
1115
1116    //     // Case 2: IDNA (Unicode in domain)
1117    //     let relative_domain = "https://münchen.com/";
1118    //     let resolved_domain = resolve_uri(relative_domain, &base);
1119    //     assert!(
1120    //         resolved_domain.is_some(),
1121    //         "Should resolve URI with unicode domain"
1122    //     );
1123    //     // München is encoded as punycode: xn--mnchen-3ya
1124    //     assert_eq!(
1125    //         resolved_domain.unwrap().to_string(),
1126    //         "https://xn--mnchen-3ya.com/"
1127    //     );
1128    // }
1129}