diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index 936bdd41a..3ab30e916 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -60,7 +60,7 @@ use quinn::VarInt; use tokio::time::Sleep; use tower::util::BoxCloneSyncServiceLayer; use tower::{Layer, Service}; -use tower_http::follow_redirect::FollowRedirect; +use tower_http::follow_redirect::{FollowRedirect, UriAndPolicyExtensions}; /// An asynchronous `Client` to make Requests with. /// @@ -995,7 +995,7 @@ impl ClientBuilder { p }; - let hyper = FollowRedirect::with_policy(hyper_service, policy.clone()); + let hyper = FollowRedirect::with_policy_extension(hyper_service, policy.clone()); Ok(Client { inner: Arc::new(ClientRef { @@ -1015,7 +1015,7 @@ impl ClientBuilder { config.pool_idle_timeout, config.cookie_store, ); - Some(FollowRedirect::with_policy(h3_service, policy)) + Some(FollowRedirect::with_policy_extension(h3_service, policy)) } None => None, }, @@ -2762,9 +2762,9 @@ struct ClientRef { #[cfg(feature = "cookies")] cookie_store: Option>, headers: HeaderMap, - hyper: FollowRedirect, + hyper: FollowRedirect, #[cfg(feature = "http3")] - h3_client: Option>, + h3_client: Option>, referer: bool, request_timeout: RequestConfig, read_timeout: Option, @@ -2845,9 +2845,23 @@ pin_project! { } enum ResponseFuture { - Default(tower_http::follow_redirect::ResponseFuture), + Default( + tower_http::follow_redirect::ResponseFuture< + HyperService, + Body, + TowerRedirectPolicy, + UriAndPolicyExtensions, + >, + ), #[cfg(feature = "http3")] - H3(tower_http::follow_redirect::ResponseFuture), + H3( + tower_http::follow_redirect::ResponseFuture< + H3Client, + Body, + TowerRedirectPolicy, + UriAndPolicyExtensions, + >, + ), } impl PendingRequest { diff --git a/src/async_impl/response.rs b/src/async_impl/response.rs index 4c0d52727..a68ca4bf9 100644 --- a/src/async_impl/response.rs +++ b/src/async_impl/response.rs @@ -12,6 +12,7 @@ use serde::de::DeserializeOwned; #[cfg(feature = "json")] use serde_json; use tokio::time::Sleep; +use tower_http::follow_redirect::FollowedPolicy; use url::Url; use super::body::Body; @@ -19,6 +20,7 @@ use super::decoder::{Accepts, Decoder}; use crate::async_impl::body::ResponseBody; #[cfg(feature = "cookies")] use crate::cookie; +use crate::redirect::TowerRedirectPolicy; #[cfg(feature = "charset")] use encoding_rs::{Encoding, UTF_8}; @@ -116,6 +118,23 @@ impl Response { &self.url } + /// Get all the intermediate `Url`s traversed by redirects. + #[inline] + pub fn history(&self) -> &[Url] { + self.extensions() + .get::>() + .map_or(&[], |p| &p.0.urls) + } + + /// Get all the `Url`s, in sequential order, that were requested, + /// including any redirects and the final url. + #[inline] + pub fn all_urls(&self) -> impl Iterator { + self.history() + .iter() + .chain(std::iter::once(self.url.as_ref())) + } + /// Get the remote address used to get this `Response`. pub fn remote_addr(&self) -> Option { self.res @@ -509,5 +528,7 @@ mod tests { assert_eq!(response.status(), 200); assert_eq!(*response.url(), url); + assert!(response.history().is_empty()); + assert_eq!(response.all_urls().collect::>(), vec![&url]); } } diff --git a/src/blocking/response.rs b/src/blocking/response.rs index 86c81772c..5fbced269 100644 --- a/src/blocking/response.rs +++ b/src/blocking/response.rs @@ -163,6 +163,19 @@ impl Response { self.inner.url() } + /// Get all the intermediate `Url`s traversed by redirects. + #[inline] + pub fn history(&self) -> &[Url] { + self.inner.history() + } + + /// Get all the `Url`s, in sequential order, that were requested, + /// including any redirects and the final url. + #[inline] + pub fn all_urls(&self) -> impl Iterator { + self.inner.all_urls() + } + /// Get the remote address used to get this `Response`. /// /// # Example diff --git a/src/redirect.rs b/src/redirect.rs index d801eb5ac..0301b9e8c 100644 --- a/src/redirect.rs +++ b/src/redirect.rs @@ -265,7 +265,7 @@ impl StdError for TooManyRedirects {} pub(crate) struct TowerRedirectPolicy { policy: Arc, referer: bool, - urls: Vec, + pub(crate) urls: Vec, https_only: bool, } diff --git a/tests/blocking.rs b/tests/blocking.rs index 4f22ea27c..4be316d8b 100644 --- a/tests/blocking.rs +++ b/tests/blocking.rs @@ -13,6 +13,7 @@ fn test_response_text() { let url = format!("http://{}/text", server.addr()); let res = reqwest::blocking::get(&url).unwrap(); assert_eq!(res.url().as_str(), &url); + assert!(res.history().is_empty()); assert_eq!(res.status(), reqwest::StatusCode::OK); assert_eq!(res.content_length(), Some(5)); @@ -55,6 +56,7 @@ fn test_response_non_utf_8_text() { let url = format!("http://{}/text", server.addr()); let res = reqwest::blocking::get(&url).unwrap(); assert_eq!(res.url().as_str(), &url); + assert!(res.history().is_empty()); assert_eq!(res.status(), reqwest::StatusCode::OK); assert_eq!(res.content_length(), Some(4)); @@ -71,6 +73,7 @@ fn test_response_json() { let url = format!("http://{}/json", server.addr()); let res = reqwest::blocking::get(&url).unwrap(); assert_eq!(res.url().as_str(), &url); + assert!(res.history().is_empty()); assert_eq!(res.status(), reqwest::StatusCode::OK); assert_eq!(res.content_length(), Some(7)); @@ -85,6 +88,7 @@ fn test_response_copy_to() { let url = format!("http://{}/1", server.addr()); let mut res = reqwest::blocking::get(&url).unwrap(); assert_eq!(res.url().as_str(), &url); + assert!(res.history().is_empty()); assert_eq!(res.status(), reqwest::StatusCode::OK); let mut dst = Vec::new(); @@ -100,6 +104,7 @@ fn test_get() { let res = reqwest::blocking::get(&url).unwrap(); assert_eq!(res.url().as_str(), &url); + assert!(res.history().is_empty()); assert_eq!(res.status(), reqwest::StatusCode::OK); assert_eq!(res.remote_addr(), Some(server.addr())); @@ -126,6 +131,7 @@ fn test_post() { .unwrap(); assert_eq!(res.url().as_str(), &url); + assert!(res.history().is_empty()); assert_eq!(res.status(), reqwest::StatusCode::OK); } @@ -155,6 +161,7 @@ fn test_post_form() { .expect("request send"); assert_eq!(res.url().as_str(), &url); + assert!(res.history().is_empty()); assert_eq!(res.status(), reqwest::StatusCode::OK); } @@ -217,6 +224,7 @@ fn test_default_headers() { let res = client.get(&url).send().unwrap(); assert_eq!(res.url().as_str(), &url); + assert!(res.history().is_empty()); assert_eq!(res.status(), reqwest::StatusCode::OK); } @@ -251,6 +259,7 @@ fn test_override_default_headers() { .unwrap(); assert_eq!(res.url().as_str(), &url); + assert!(res.history().is_empty()); assert_eq!(res.status(), reqwest::StatusCode::OK); } @@ -276,6 +285,7 @@ fn test_appended_headers_not_overwritten() { .unwrap(); assert_eq!(res.url().as_str(), &url); + assert!(res.history().is_empty()); assert_eq!(res.status(), reqwest::StatusCode::OK); // make sure this also works with default headers @@ -299,6 +309,7 @@ fn test_appended_headers_not_overwritten() { .unwrap(); assert_eq!(res.url().as_str(), &url); + assert!(res.history().is_empty()); assert_eq!(res.status(), reqwest::StatusCode::OK); } @@ -403,6 +414,7 @@ fn test_response_no_tls_info_for_http() { let res = client.get(&url).send().unwrap(); assert_eq!(res.url().as_str(), &url); + assert!(res.history().is_empty()); assert_eq!(res.status(), reqwest::StatusCode::OK); assert_eq!(res.content_length(), Some(5)); let tls_info = res.extensions().get::(); diff --git a/tests/client.rs b/tests/client.rs index 307892c98..450c82275 100644 --- a/tests/client.rs +++ b/tests/client.rs @@ -57,6 +57,7 @@ async fn auto_headers() { .unwrap(); assert_eq!(res.url().as_str(), &url); + assert!(res.history().is_empty()); assert_eq!(res.status(), reqwest::StatusCode::OK); assert_eq!(res.remote_addr(), Some(server.addr())); } diff --git a/tests/redirect.rs b/tests/redirect.rs index 186717358..f99d90ea4 100644 --- a/tests/redirect.rs +++ b/tests/redirect.rs @@ -4,6 +4,7 @@ mod support; use http_body_util::BodyExt; use reqwest::Body; use support::server; +use url::Url; #[tokio::test] async fn test_redirect_301_and_302_and_303_changes_post_to_get() { @@ -34,6 +35,14 @@ async fn test_redirect_301_and_302_and_303_changes_post_to_get() { let dst = format!("http://{}/{}", redirect.addr(), "dst"); let res = client.post(&url).send().await.unwrap(); assert_eq!(res.url().as_str(), dst); + assert_eq!( + res.history().iter().map(Url::as_str).collect::>(), + vec![&url] + ); + assert_eq!( + res.all_urls().map(Url::as_str).collect::>(), + vec![&url, &dst] + ); assert_eq!(res.status(), reqwest::StatusCode::OK); assert_eq!( res.headers().get(reqwest::header::SERVER).unwrap(), @@ -70,6 +79,14 @@ async fn test_redirect_307_and_308_tries_to_get_again() { let dst = format!("http://{}/{}", redirect.addr(), "dst"); let res = client.get(&url).send().await.unwrap(); assert_eq!(res.url().as_str(), dst); + assert_eq!( + res.history().iter().map(Url::as_str).collect::>(), + vec![&url] + ); + assert_eq!( + res.all_urls().map(Url::as_str).collect::>(), + vec![&url, &dst] + ); assert_eq!(res.status(), reqwest::StatusCode::OK); assert_eq!( res.headers().get(reqwest::header::SERVER).unwrap(), @@ -119,6 +136,14 @@ async fn test_redirect_307_and_308_tries_to_post_again() { let dst = format!("http://{}/{}", redirect.addr(), "dst"); let res = client.post(&url).body("Hello").send().await.unwrap(); assert_eq!(res.url().as_str(), dst); + assert_eq!( + res.history().iter().map(Url::as_str).collect::>(), + vec![&url] + ); + assert_eq!( + res.all_urls().map(Url::as_str).collect::>(), + vec![&url, &dst] + ); assert_eq!(res.status(), reqwest::StatusCode::OK); assert_eq!( res.headers().get(reqwest::header::SERVER).unwrap(), @@ -163,6 +188,11 @@ fn test_redirect_307_does_not_try_if_reader_cannot_reset() { .send() .unwrap(); assert_eq!(res.url().as_str(), url); + assert!(res.history().is_empty()); + assert_eq!( + res.all_urls().map(Url::as_str).collect::>(), + vec![&url] + ); assert_eq!(res.status(), code); } } @@ -253,6 +283,11 @@ async fn test_redirect_policy_can_stop_redirects_without_an_error() { .unwrap(); assert_eq!(res.url().as_str(), url); + assert!(res.history().is_empty()); + assert_eq!( + res.all_urls().map(Url::as_str).collect::>(), + vec![&url] + ); assert_eq!(res.status(), reqwest::StatusCode::FOUND); } @@ -298,6 +333,11 @@ async fn test_invalid_location_stops_redirect_gh484() { let res = reqwest::get(&url).await.unwrap(); assert_eq!(res.url().as_str(), url); + assert!(res.history().is_empty()); + assert_eq!( + res.all_urls().map(Url::as_str).collect::>(), + vec![&url] + ); assert_eq!(res.status(), reqwest::StatusCode::FOUND); } @@ -346,6 +386,14 @@ async fn test_redirect_302_with_set_cookies() { let res = client.get(&url).send().await.unwrap(); assert_eq!(res.url().as_str(), dst); + assert_eq!( + res.history().iter().map(Url::as_str).collect::>(), + vec![&url] + ); + assert_eq!( + res.all_urls().map(Url::as_str).collect::>(), + vec![&url, &dst] + ); assert_eq!(res.status(), reqwest::StatusCode::OK); }