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//! Request [`Extensions`] are carried over to redirected
10//! requests according to the configured [`RedirectExtensionsBehaviour`] (preserved — i.e. the
11//! same store is shared — by default). The request body cannot always be cloned. When the original body is
12//! known to be empty by [`StreamingBody::size_hint`], the middleware uses the `Default`
13//! implementation of the body type to create a new request body. If you know that the body can be
14//! cloned in some way, you can tell the middleware to clone it by configuring a [`policy`].
15//!
16//! # Examples
17//!
18//! ## Basic usage
19//!
20//! ```
21//! use rama_core::service::service_fn;
22//! use rama_core::{extensions::ExtensionsRef, Service, Layer};
23//! use rama_http::{Body, Request, Response, StatusCode, header};
24//! use rama_http::layer::follow_redirect::{FollowRedirectLayer, RequestUri};
25//!
26//! # #[tokio::main]
27//! # async fn main() -> Result<(), std::convert::Infallible> {
28//! # let http_client = service_fn(async |req: Request| {
29//! #     let dest = "https://www.rust-lang.org/";
30//! #     let mut res = Response::builder();
31//! #     if req.uri().as_str() != dest {
32//! #         res = res
33//! #             .status(StatusCode::MOVED_PERMANENTLY)
34//! #             .header(header::LOCATION, dest);
35//! #     }
36//! #     Ok::<_, std::convert::Infallible>(res.body(Body::empty()).unwrap())
37//! # });
38//! let mut client = FollowRedirectLayer::new().into_layer(http_client);
39//!
40//! let request = Request::builder()
41//!     .uri("https://rust-lang.org/")
42//!     .body(Body::empty())
43//!     .unwrap();
44//!
45//! let response = client.serve(request).await?;
46//! // Get the final request URI.
47//! assert_eq!(response.extensions().get_ref::<RequestUri>().unwrap().0.as_str(), "https://www.rust-lang.org/");
48//! # Ok(())
49//! # }
50//! ```
51//!
52//! ## Customizing the `Policy`
53//!
54//! You can use a [`Policy`] value to customize how the middleware handles redirections.
55//!
56//! ```
57//! # #![allow(unused)]
58//!
59//! # use std::convert::Infallible;
60//! use rama_core::service::service_fn;
61//! use rama_core::layer::MapErrLayer;
62//! use rama_core::{Service, Layer};
63//! use rama_http::{Body, Request, Response};
64//! use rama_http::layer::follow_redirect::{
65//!     policy::{self, PolicyExt},
66//!     FollowRedirectLayer,
67//! };
68//! use rama_core::error::BoxError;
69//!
70//! #[derive(Debug)]
71//! enum MyError {
72//!     TooManyRedirects,
73//!     Other(BoxError),
74//! }
75//!
76//! impl MyError {
77//!     fn from_std(err: impl std::error::Error + Send + Sync + 'static) -> Self {
78//!         Self::Other(BoxError::from(err))
79//!     }
80//!
81//! }
82//!
83//! # #[tokio::main]
84//! # async fn main() -> Result<(), MyError> {
85//! # let http_client = service_fn(async |_: Request| Ok::<_, Infallible>(Response::new(Body::empty())));
86//! let policy = policy::Limited::new(10) // Set the maximum number of redirections to 10.
87//!     // Return an error when the limit was reached.
88//!     .or::<_, (), _>(policy::redirect_fn(|_| Err(MyError::TooManyRedirects)))
89//!     // Do not follow cross-origin redirections, and return the redirection responses as-is.
90//!     .and::<_, (), _>(policy::SameOrigin::new());
91//!
92//! let client = (
93//!     FollowRedirectLayer::with_policy(policy),
94//!     MapErrLayer::new(MyError::from_std),
95//! ).into_layer(http_client);
96//!
97//! // ...
98//! _ = client.serve(Request::default()).await?;
99//! # Ok(())
100//! # }
101//! ```
102
103pub mod policy;
104
105use crate::{Method, Request, Response, StatusCode, StreamingBody, header::LOCATION};
106use iri_string::types::{UriAbsoluteString, UriReferenceStr};
107use rama_core::{
108    Layer, Service,
109    extensions::{Extension, Extensions, ExtensionsRef},
110};
111use rama_http_types::{
112    HeaderMap,
113    header::{CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TYPE, TRANSFER_ENCODING},
114};
115use rama_net::uri::Uri;
116use rama_utils::macros::{define_inner_service_accessors, generate_set_and_with};
117use std::fmt;
118
119use self::policy::{Action, Attempt, Policy, Standard};
120
121/// Controls how request [`Extensions`] are carried over to redirected requests.
122///
123/// rama's [`Extensions`] are an append-only, parent-chained store, so this mirrors the way
124/// retries and forks are modelled elsewhere in rama rather than the boolean toggle used by
125/// upstream `tower-http`.
126///
127/// Note that, regardless of the variant, a forwarded extension can be _read_ by the redirect
128/// target (including cross-origin ones). Use [`Self::Drop`] when extensions may carry sensitive,
129/// origin-scoped data. Unlike upstream `tower-http` — whose default `Standard` policy clears
130/// extensions on a cross-origin hop — rama's [`Extensions`] are append-only, so no policy
131/// (including [`FilterCredentials`]) can strip them; isolating origin-scoped data is solely this
132/// setting's job, via [`Self::Drop`].
133///
134/// [`FilterCredentials`]: policy::FilterCredentials
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
136#[non_exhaustive]
137pub enum RedirectExtensionsBehaviour {
138    /// Share the original request's extension store with redirected requests.
139    ///
140    /// The redirected requests reference the same underlying store, so inserts made while
141    /// following a redirect are visible to the original caller as well. This is the default.
142    #[default]
143    Preserve,
144    /// Carry a [`fork`][Extensions::fork] of the request extensions to redirected requests.
145    ///
146    /// The redirected request can read every extension the original request had, but its own
147    /// inserts stay isolated and never leak back to the caller or accumulate across hops, which
148    /// mirrors rama's convention that retries/forks fork from the original request.
149    Fork,
150    /// Drop all extensions on redirected requests; each starts with an empty store.
151    Drop,
152}
153
154impl RedirectExtensionsBehaviour {
155    /// Derive the [`Extensions`] for a redirected request from the original request's `source`.
156    fn redirect_extensions(self, source: &Extensions) -> Extensions {
157        match self {
158            Self::Preserve => source.clone(),
159            Self::Fork => source.fork(),
160            Self::Drop => Extensions::new(),
161        }
162    }
163}
164
165/// [`Layer`] for retrying requests with a [`Service`] to follow redirection responses.
166///
167/// See the [module docs](self) for more details.
168#[derive(Clone)]
169pub struct FollowRedirectLayer<P = Standard> {
170    policy: P,
171    extensions_behaviour: RedirectExtensionsBehaviour,
172}
173
174impl FollowRedirectLayer {
175    /// Create a new [`FollowRedirectLayer`] with a [`Standard`] redirection policy.
176    #[must_use]
177    pub fn new() -> Self {
178        Self::default()
179    }
180}
181
182impl Default for FollowRedirectLayer {
183    fn default() -> Self {
184        Self::with_policy(Standard::default())
185    }
186}
187
188impl<P: fmt::Debug> fmt::Debug for FollowRedirectLayer<P> {
189    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190        f.debug_struct("FollowRedirectLayer")
191            .field("policy", &self.policy)
192            .field("extensions_behaviour", &self.extensions_behaviour)
193            .finish()
194    }
195}
196
197impl<P> FollowRedirectLayer<P> {
198    /// Create a new [`FollowRedirectLayer`] with the given redirection [`Policy`].
199    pub fn with_policy(policy: P) -> Self {
200        Self {
201            policy,
202            extensions_behaviour: RedirectExtensionsBehaviour::default(),
203        }
204    }
205
206    generate_set_and_with! {
207        /// Set how request [`Extensions`] are carried over to redirected requests.
208        ///
209        /// Defaults to [`RedirectExtensionsBehaviour::Preserve`].
210        pub fn redirect_extensions_behaviour(
211            mut self,
212            behaviour: RedirectExtensionsBehaviour,
213        ) -> Self {
214            self.extensions_behaviour = behaviour;
215            self
216        }
217    }
218}
219
220impl<S, P> Layer<S> for FollowRedirectLayer<P>
221where
222    S: Clone,
223    P: Clone,
224{
225    type Service = FollowRedirect<S, P>;
226
227    fn layer(&self, inner: S) -> Self::Service {
228        FollowRedirect {
229            inner,
230            policy: self.policy.clone(),
231            extensions_behaviour: self.extensions_behaviour,
232        }
233    }
234
235    fn into_layer(self, inner: S) -> Self::Service {
236        FollowRedirect {
237            inner,
238            policy: self.policy,
239            extensions_behaviour: self.extensions_behaviour,
240        }
241    }
242}
243
244/// Middleware that retries requests with a [`Service`] to follow redirection responses.
245///
246/// See the [module docs](self) for more details.
247#[derive(Debug, Clone)]
248pub struct FollowRedirect<S, P = Standard> {
249    inner: S,
250    policy: P,
251    extensions_behaviour: RedirectExtensionsBehaviour,
252}
253
254impl<S> FollowRedirect<S> {
255    /// Create a new [`FollowRedirect`] with a [`Standard`] redirection policy.
256    pub fn new(inner: S) -> Self {
257        Self::with_policy(inner, Standard::default())
258    }
259}
260
261impl<S, P> FollowRedirect<S, P> {
262    /// Create a new [`FollowRedirect`] with the given redirection [`Policy`].
263    pub fn with_policy(inner: S, policy: P) -> Self {
264        Self {
265            inner,
266            policy,
267            extensions_behaviour: RedirectExtensionsBehaviour::default(),
268        }
269    }
270
271    generate_set_and_with! {
272        /// Set how request [`Extensions`] are carried over to redirected requests.
273        ///
274        /// See [`FollowRedirectLayer::with_redirect_extensions_behaviour`].
275        pub fn redirect_extensions_behaviour(
276            mut self,
277            behaviour: RedirectExtensionsBehaviour,
278        ) -> Self {
279            self.extensions_behaviour = behaviour;
280            self
281        }
282    }
283
284    define_inner_service_accessors!();
285}
286
287impl<ReqBody, ResBody, S, P> Service<Request<ReqBody>> for FollowRedirect<S, P>
288where
289    S: Service<Request<ReqBody>, Output = Response<ResBody>>,
290    ReqBody: StreamingBody + Default + Send + 'static,
291    ResBody: Send + 'static,
292    P: Policy<ReqBody, S::Error> + Clone,
293{
294    type Output = Response<ResBody>;
295    type Error = S::Error;
296
297    fn serve(
298        &self,
299
300        mut req: Request<ReqBody>,
301    ) -> impl Future<Output = Result<Self::Output, Self::Error>> {
302        let mut method = req.method().clone();
303        let mut uri = req.uri().clone();
304        let version = req.version();
305        let mut headers = req.headers().clone();
306
307        let mut policy = self.policy.clone();
308
309        let mut body = BodyRepr::None;
310        body.try_clone_from(&mut policy, req.body());
311        policy.on_request(&mut req);
312
313        // Snapshot the request extensions to carry over to redirected requests, per the
314        // configured behaviour.
315        let extensions_behaviour = self.extensions_behaviour;
316        let extensions_source = req.extensions().clone();
317
318        let service = &self.inner;
319
320        async move {
321            loop {
322                let res = service.serve(req).await?;
323                res.extensions().insert(RequestUri(uri.clone()));
324
325                let previous_method = method.clone();
326                let drop_payload_headers = |headers: &mut HeaderMap| {
327                    for header in &[
328                        CONTENT_TYPE,
329                        CONTENT_LENGTH,
330                        CONTENT_ENCODING,
331                        TRANSFER_ENCODING,
332                    ] {
333                        headers.remove(header);
334                    }
335                };
336
337                match res.status() {
338                    StatusCode::MOVED_PERMANENTLY | StatusCode::FOUND => {
339                        // User agents MAY change the request method from POST to GET
340                        // (RFC 7231 section 6.4.2. and 6.4.3.).
341                        if method == Method::POST {
342                            method = Method::GET;
343                            body = BodyRepr::Empty;
344                            drop_payload_headers(&mut headers);
345                        }
346                    }
347                    StatusCode::SEE_OTHER => {
348                        // A user agent can perform a GET or HEAD request (RFC 7231 section 6.4.4.).
349                        if method != Method::HEAD {
350                            method = Method::GET;
351                        }
352                        body = BodyRepr::Empty;
353                        drop_payload_headers(&mut headers);
354                    }
355                    StatusCode::TEMPORARY_REDIRECT | StatusCode::PERMANENT_REDIRECT => {}
356                    _ => return Ok(res),
357                };
358
359                let Some(taken_body) = body.take() else {
360                    return Ok(res);
361                };
362
363                let location = res
364                    .headers()
365                    .get(&LOCATION)
366                    .and_then(|loc| resolve_uri(std::str::from_utf8(loc.as_bytes()).ok()?, &uri));
367                let Some(location) = location else {
368                    return Ok(res);
369                };
370
371                let attempt = Attempt {
372                    status: res.status(),
373                    method: &method,
374                    location: &location,
375                    previous_method: &previous_method,
376                    previous: &uri,
377                };
378                match policy.redirect(&attempt)? {
379                    Action::Follow => {
380                        uri = location;
381                        body.try_clone_from(&mut policy, &taken_body);
382
383                        req = Request::new(taken_body);
384                        *req.uri_mut() = uri.clone();
385                        *req.method_mut() = method.clone();
386                        *req.version_mut() = version;
387                        *req.headers_mut() = headers.clone();
388                        req.set_extensions(
389                            extensions_behaviour.redirect_extensions(&extensions_source),
390                        );
391                        policy.on_request(&mut req);
392                        // Carry the filtered headers forward so anything dropped on this hop
393                        // stays dropped on the next one (e.g. credentials after a cross-origin
394                        // hop must not resurrect on a later same-origin hop).
395                        headers = req.headers().clone();
396                    }
397                    Action::Stop => return Ok(res),
398                }
399            }
400        }
401    }
402}
403
404/// Response [`Extensions`] value that represents the effective request URI of
405/// a response returned by a [`FollowRedirect`] middleware.
406///
407/// The value differs from the original request's effective URI if the middleware has followed
408/// redirections.
409#[derive(Debug, Clone, Extension)]
410#[extension(tags(http))]
411pub struct RequestUri(pub Uri);
412
413#[derive(Debug)]
414enum BodyRepr<B> {
415    Some(B),
416    Empty,
417    None,
418}
419
420impl<B> BodyRepr<B>
421where
422    B: StreamingBody + Default,
423{
424    fn take(&mut self) -> Option<B> {
425        match std::mem::replace(self, Self::None) {
426            Self::Some(body) => Some(body),
427            Self::Empty => {
428                *self = Self::Empty;
429                Some(B::default())
430            }
431            Self::None => None,
432        }
433    }
434
435    fn try_clone_from<P, E>(&mut self, policy: &mut P, body: &B)
436    where
437        P: Policy<B, E>,
438    {
439        match self {
440            Self::Some(_) | Self::Empty => {}
441            Self::None => {
442                if let Some(body) = clone_body(policy, body) {
443                    *self = Self::Some(body);
444                }
445            }
446        }
447    }
448}
449
450fn clone_body<P, B, E>(policy: &mut P, body: &B) -> Option<B>
451where
452    P: Policy<B, E>,
453    B: StreamingBody + Default,
454{
455    if body.size_hint().exact() == Some(0) {
456        Some(B::default())
457    } else {
458        policy.clone_body(body)
459    }
460}
461
462/// Try to resolve a URI reference `relative` against a base URI `base`.
463fn resolve_uri(relative: &str, base: &Uri) -> Option<Uri> {
464    let relative = UriReferenceStr::new(relative).ok()?;
465    let base = UriAbsoluteString::try_from(base.to_string()).ok()?;
466    let uri = relative.resolve_against(&base).to_string();
467    Uri::try_from(uri).ok()
468}
469
470/* // ^TODO replace w/ something similar to
471let base_url = Url::parse(&base.to_string()).ok()?;
472let resolved = base_url.join(relative).ok()?;
473Uri::try_from(String::from(resolved)).ok()
474*/
475
476#[cfg(test)]
477mod tests {
478    use super::{policy::*, *};
479    use crate::{Body, header::LOCATION};
480    use rama_core::Layer;
481    use rama_core::extensions::ExtensionsRef;
482    use rama_core::service::service_fn;
483    use std::convert::Infallible;
484
485    #[tokio::test]
486    async fn follows() {
487        let svc = FollowRedirectLayer::with_policy(Action::Follow).into_layer(service_fn(handle));
488        let req = Request::builder()
489            .uri("http://example.com/42")
490            .body(Body::empty())
491            .unwrap();
492        let res = svc.serve(req).await.unwrap();
493        assert_eq!(*res.body(), 0);
494        assert_eq!(
495            res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
496            "http://example.com/0"
497        );
498    }
499
500    #[tokio::test]
501    async fn stops() {
502        let svc = FollowRedirectLayer::with_policy(Action::Stop).into_layer(service_fn(handle));
503        let req = Request::builder()
504            .uri("http://example.com/42")
505            .body(Body::empty())
506            .unwrap();
507        let res = svc.serve(req).await.unwrap();
508        assert_eq!(*res.body(), 42);
509        assert_eq!(
510            res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
511            "http://example.com/42"
512        );
513    }
514
515    #[tokio::test]
516    async fn limited() {
517        let svc = FollowRedirectLayer::with_policy(Limited::new(10)).into_layer(service_fn(handle));
518        let req = Request::builder()
519            .uri("http://example.com/42")
520            .body(Body::empty())
521            .unwrap();
522        let res = svc.serve(req).await.unwrap();
523        assert_eq!(*res.body(), 42 - 10);
524        assert_eq!(
525            res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
526            "http://example.com/32"
527        );
528    }
529
530    /// A server with an endpoint `/{n}` which redirects to `/{n-1}` unless `n` equals zero,
531    /// returning `n` as the response body.
532    async fn handle<B>(req: Request<B>) -> Result<Response<u64>, Infallible> {
533        let n: u64 = req
534            .uri()
535            .first_path_segment()
536            .and_then(|segment| segment.as_encoded_str().parse().ok())
537            .unwrap();
538        let mut res = Response::builder();
539        if n > 0 {
540            res = res
541                .status(StatusCode::MOVED_PERMANENTLY)
542                .header(LOCATION, format!("/{}", n - 1));
543        }
544        Ok::<_, Infallible>(res.body(n).unwrap())
545    }
546
547    #[derive(Clone, Debug, PartialEq, rama_core::extensions::Extension)]
548    struct Marker(u32);
549
550    /// Like [`handle`] but also copies a `Marker` request extension onto the response, so a test
551    /// can observe whether it reached the (final, redirected) request.
552    async fn handle_marker<B>(req: Request<B>) -> Result<Response<u64>, Infallible> {
553        let n: u64 = req
554            .uri()
555            .first_path_segment()
556            .and_then(|segment| segment.as_encoded_str().parse().ok())
557            .unwrap();
558        let mut res = Response::builder();
559        if n > 0 {
560            res = res
561                .status(StatusCode::MOVED_PERMANENTLY)
562                .header(LOCATION, format!("/{}", n - 1));
563        }
564        let res = res.body(n).unwrap();
565        if let Some(marker) = req.extensions().get_ref::<Marker>() {
566            res.extensions().insert(marker.clone());
567        }
568        Ok::<_, Infallible>(res)
569    }
570
571    #[tokio::test]
572    async fn preserves_extensions_by_default() {
573        let svc = FollowRedirectLayer::new().into_layer(service_fn(handle_marker));
574        let req = Request::builder()
575            .uri("http://example.com/3")
576            .body(Body::empty())
577            .unwrap();
578        req.extensions().insert(Marker(7));
579        let res = svc.serve(req).await.unwrap();
580        // The default (Preserve) shares the original store, so every redirected request reads it.
581        assert_eq!(res.extensions().get_ref::<Marker>(), Some(&Marker(7)));
582    }
583
584    #[tokio::test]
585    async fn preserve_shares_extensions() {
586        let svc = FollowRedirectLayer::new()
587            .with_redirect_extensions_behaviour(RedirectExtensionsBehaviour::Preserve)
588            .into_layer(service_fn(handle_marker));
589        let req = Request::builder()
590            .uri("http://example.com/3")
591            .body(Body::empty())
592            .unwrap();
593        req.extensions().insert(Marker(7));
594        let res = svc.serve(req).await.unwrap();
595        assert_eq!(res.extensions().get_ref::<Marker>(), Some(&Marker(7)));
596    }
597
598    /// Drives a cross-origin redirect chain and echoes, via `x-saw-cookie`, whether the incoming
599    /// request still carried a `Cookie`:
600    /// `a.example.com` → `b.example.com/second` (cross-origin) → `b.example.com/final` (same-origin).
601    async fn handle_cookie_chain<B>(req: Request<B>) -> Result<Response<u64>, Infallible> {
602        let host = req.uri().host_str();
603        let path = req.uri().path_ref_or_root();
604        let location = if host.as_deref() == Some("a.example.com") {
605            Some("http://b.example.com/second")
606        } else if host.as_deref() == Some("b.example.com") && path == "/second" {
607            Some("http://b.example.com/final")
608        } else {
609            None
610        };
611        let mut res = Response::builder();
612        if let Some(location) = location {
613            res = res
614                .status(StatusCode::MOVED_PERMANENTLY)
615                .header(LOCATION, location);
616        }
617        let mut res = res.body(0u64).unwrap();
618        if req.headers().contains_key(crate::header::COOKIE) {
619            res.headers_mut()
620                .insert("x-saw-cookie", crate::HeaderValue::from_static("1"));
621        }
622        Ok::<_, Infallible>(res)
623    }
624
625    #[tokio::test]
626    async fn credentials_do_not_resurrect_after_cross_origin() {
627        // Regression for the cumulative-filtering half of tower-http #706: the default Standard
628        // policy strips Cookie on the cross-origin a→b hop; it must NOT reappear on the later
629        // same-origin b→b hop just because the original header snapshot is replayed.
630        let svc = FollowRedirectLayer::default().into_layer(service_fn(handle_cookie_chain));
631        let req = Request::builder()
632            .uri("http://a.example.com/")
633            .header(crate::header::COOKIE, "session=secret")
634            .body(Body::empty())
635            .unwrap();
636        let res = svc.serve(req).await.unwrap();
637        assert!(
638            !res.headers().contains_key("x-saw-cookie"),
639            "Cookie resurrected on a same-origin hop after being dropped cross-origin",
640        );
641        assert_eq!(
642            res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
643            "http://b.example.com/final"
644        );
645    }
646
647    #[tokio::test]
648    async fn drop_extensions_opt_out() {
649        let svc = FollowRedirectLayer::new()
650            .with_redirect_extensions_behaviour(RedirectExtensionsBehaviour::Drop)
651            .into_layer(service_fn(handle_marker));
652        let req = Request::builder()
653            .uri("http://example.com/3")
654            .body(Body::empty())
655            .unwrap();
656        req.extensions().insert(Marker(7));
657        let res = svc.serve(req).await.unwrap();
658        // Dropping extensions means the final, redirected request never sees the marker.
659        assert!(res.extensions().get_ref::<Marker>().is_none());
660    }
661
662    #[tokio::test]
663    async fn test_301_redirects() {
664        let policy = policy::redirect_fn(|attempt| -> Result<_, Infallible> {
665            if attempt.previous_method() == Method::POST && attempt.method() == Method::GET {
666                Ok(Action::Stop)
667            } else {
668                Ok(Action::Follow)
669            }
670        });
671        let svc = FollowRedirectLayer::with_policy(policy).into_layer(service_fn(redirections));
672
673        // A POST request with a 301 redirection should turn into a GET
674        // request, and the policy should stop the redirection.
675        {
676            let req = Request::builder()
677                .method(Method::POST)
678                .uri("http://example.com/301")
679                .body(Body::empty())
680                .unwrap();
681            let res = svc.clone().serve(req).await.unwrap();
682            assert_eq!(*res.body(), "/target/301");
683            assert_eq!(
684                res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
685                "http://example.com/301"
686            );
687        }
688
689        // A GET request with a 301 redirection should remain a GET
690        // request, and the policy should allow the redirection.
691        {
692            let req = Request::builder()
693                .method(Method::GET)
694                .uri("http://example.com/301")
695                .body(Body::empty())
696                .unwrap();
697            let res = svc.clone().serve(req).await.unwrap();
698            assert_eq!(*res.body(), "/target/301/final");
699            assert_eq!(
700                res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
701                "http://example.com/target/301"
702            );
703        }
704    }
705
706    #[tokio::test]
707    async fn test_302_redirects() {
708        let policy = policy::redirect_fn(|attempt| -> Result<_, Infallible> {
709            if attempt.previous_method() != attempt.method() {
710                Ok(Action::Stop)
711            } else {
712                Ok(Action::Follow)
713            }
714        });
715        let svc = FollowRedirectLayer::with_policy(policy).into_layer(service_fn(redirections));
716
717        // A POST request with a 302 redirection should turn into a GET
718        // request, and the policy should stop the redirection.
719        {
720            let req = Request::builder()
721                .method(Method::POST)
722                .uri("http://example.com/302")
723                .body(Body::empty())
724                .unwrap();
725            let res = svc.clone().serve(req).await.unwrap();
726            assert_eq!(*res.body(), "/target/302");
727            assert_eq!(
728                res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
729                "http://example.com/302"
730            );
731        }
732
733        // A PUT request with a 302 redirection should remain a PUT
734        // request, and the policy should allow the redirection.
735        {
736            let req = Request::builder()
737                .method(Method::PUT)
738                .uri("http://example.com/302")
739                .body(Body::empty())
740                .unwrap();
741            let res = svc.clone().serve(req).await.unwrap();
742            assert_eq!(*res.body(), "/target/302/final");
743            assert_eq!(
744                res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
745                "http://example.com/target/302"
746            );
747        }
748
749        // A HEAD request with a 302 redirection should remain a HEAD
750        // request, and the policy should allow the redirection.
751        {
752            let req = Request::builder()
753                .method(Method::HEAD)
754                .uri("http://example.com/302")
755                .body(Body::empty())
756                .unwrap();
757            let res = svc.clone().serve(req).await.unwrap();
758            assert_eq!(*res.body(), "/target/302/final");
759            assert_eq!(
760                res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
761                "http://example.com/target/302"
762            );
763        }
764    }
765
766    #[tokio::test]
767    async fn test_303_redirects() {
768        let policy = policy::redirect_fn(|attempt| -> Result<_, Infallible> {
769            if attempt.previous_method() != attempt.method() {
770                Ok(Action::Stop)
771            } else {
772                Ok(Action::Follow)
773            }
774        });
775        let svc = FollowRedirectLayer::with_policy(policy).into_layer(service_fn(redirections));
776
777        // A POST request with a 303 redirection should turn into a GET
778        // request, and the policy should stop the redirection.
779        {
780            let req = Request::builder()
781                .method(Method::POST)
782                .uri("http://example.com/303")
783                .body(Body::empty())
784                .unwrap();
785            let res = svc.clone().serve(req).await.unwrap();
786            assert_eq!(*res.body(), "/target/303");
787            assert_eq!(
788                res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
789                "http://example.com/303"
790            );
791        }
792
793        // A PUT request with a 303 redirection should turn into a GET
794        // request, and the policy should stop the redirection.
795        {
796            let req = Request::builder()
797                .method(Method::PUT)
798                .uri("http://example.com/303")
799                .body(Body::empty())
800                .unwrap();
801            let res = svc.clone().serve(req).await.unwrap();
802            assert_eq!(*res.body(), "/target/303");
803            assert_eq!(
804                res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
805                "http://example.com/303"
806            );
807        }
808
809        // A HEAD request with a 303 redirection should remain a HEAD
810        // request, and the policy should allow the redirection.
811        {
812            let req = Request::builder()
813                .method(Method::HEAD)
814                .uri("http://example.com/303")
815                .body(Body::empty())
816                .unwrap();
817            let res = svc.clone().serve(req).await.unwrap();
818            assert_eq!(*res.body(), "/target/303/final");
819            assert_eq!(
820                res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
821                "http://example.com/target/303"
822            );
823        }
824    }
825
826    #[tokio::test]
827    async fn test_307_308_redirects() {
828        let policy = policy::redirect_fn(|attempt| -> Result<_, Infallible> {
829            if attempt.previous_method() != Method::POST || attempt.method() != Method::POST {
830                Ok(Action::Stop)
831            } else {
832                Ok(Action::Follow)
833            }
834        });
835        let svc = FollowRedirectLayer::with_policy(policy).into_layer(service_fn(redirections));
836
837        // A POST request with a 307 redirection should remain a POST
838        // request, and the policy should allow the redirection.
839        {
840            let req = Request::builder()
841                .method(Method::POST)
842                .uri("http://example.com/307")
843                .body(Body::empty())
844                .unwrap();
845            let res = svc.clone().serve(req).await.unwrap();
846            assert_eq!(*res.body(), "/target/307/final");
847            assert_eq!(
848                res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
849                "http://example.com/target/307"
850            );
851        }
852
853        // A POST request with a 308 redirection should remain a POST
854        // request, and the policy should allow the redirection.
855        {
856            let req = Request::builder()
857                .method(Method::POST)
858                .uri("http://example.com/308")
859                .body(Body::empty())
860                .unwrap();
861            let res = svc.clone().serve(req).await.unwrap();
862            assert_eq!(*res.body(), "/target/308/final");
863            assert_eq!(
864                res.extensions().get_ref::<RequestUri>().unwrap().0.as_str(),
865                "http://example.com/target/308"
866            );
867        }
868    }
869
870    /// Returns different 3xx redirections based on the request's URI.
871    async fn redirections<B>(req: Request<B>) -> Result<Response<String>, Infallible> {
872        let path = req.uri().path_ref_or_root();
873        let mut res = Response::builder();
874        let body_str;
875        res = if path == "/301" {
876            let case = "/target/301";
877            body_str = case.to_owned();
878            res.status(StatusCode::MOVED_PERMANENTLY)
879                .header(LOCATION, case)
880        } else if path == "/302" {
881            let case = "/target/302";
882            body_str = case.to_owned();
883            res.status(StatusCode::FOUND).header(LOCATION, case)
884        } else if path == "/303" {
885            let case = "/target/303";
886            body_str = case.to_owned();
887            res.status(StatusCode::SEE_OTHER).header(LOCATION, case)
888        } else if path == "/307" {
889            let case = "/target/307";
890            body_str = case.to_owned();
891            res.status(StatusCode::TEMPORARY_REDIRECT)
892                .header(LOCATION, case)
893        } else if path == "/308" {
894            let case = "/target/308";
895            body_str = case.to_owned();
896            res.status(StatusCode::PERMANENT_REDIRECT)
897                .header(LOCATION, case)
898        } else {
899            body_str = format!("{path}/final");
900            res.status(StatusCode::OK)
901        };
902        Ok::<_, Infallible>(res.body(body_str).unwrap())
903    }
904
905    // TOOD: adapt + enable once we did Uri rework
906    // #[tokio::test]
907    // async fn test_resolve_uri_unicode() {
908    //     let base = Uri::from_static("https://example.com/api");
909    //     // Case 1: Unicode in path
910    //     let relative = "/café";
911    //     let resolved = resolve_uri(relative, &base);
912    //     assert!(resolved.is_some(), "Should resolve URI with unicode path");
913    //     assert_eq!(
914    //         resolved.unwrap().to_string(),
915    //         "https://example.com/caf%C3%A9"
916    //     );
917
918    //     // Case 2: IDNA (Unicode in domain)
919    //     let relative_domain = "https://münchen.com/";
920    //     let resolved_domain = resolve_uri(relative_domain, &base);
921    //     assert!(
922    //         resolved_domain.is_some(),
923    //         "Should resolve URI with unicode domain"
924    //     );
925    //     // München is encoded as punycode: xn--mnchen-3ya
926    //     assert_eq!(
927    //         resolved_domain.unwrap().to_string(),
928    //         "https://xn--mnchen-3ya.com/"
929    //     );
930    // }
931}