Skip to content

Commit 7a1c86a

Browse files
authored
feat(redirect): allow custom redirects to access response headers (#916)
1 parent 356950d commit 7a1c86a

4 files changed

Lines changed: 88 additions & 62 deletions

File tree

examples/request_with_redirect.rs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,20 @@ use wreq::redirect::Policy;
22

33
#[tokio::main]
44
async fn main() -> wreq::Result<()> {
5-
tracing_subscriber::fmt()
6-
.with_max_level(tracing::Level::TRACE)
7-
.init();
8-
95
// Use the API you're already familiar with
10-
let resp = wreq::get("http://google.com/")
11-
.redirect(Policy::default())
6+
let resp = wreq::get("https://google.com/")
7+
.redirect(Policy::custom(|attempt| {
8+
// we can inspect the redirect attempt
9+
println!(
10+
"Redirecting (status: {}) to {:?} and headers: {:#?}",
11+
attempt.status(),
12+
attempt.uri(),
13+
attempt.headers()
14+
);
15+
16+
// we can follow redirects as normal
17+
attempt.follow()
18+
}))
1219
.send()
1320
.await?;
1421
println!("{}", resp.text().await?);

src/client/layer/redirect/future.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,11 @@ where
121121

122122
let attempt = Attempt {
123123
status: res.status(),
124+
headers: res.headers(),
124125
location: &location,
125126
previous: uri,
126127
};
128+
127129
match policy.redirect(&attempt)? {
128130
Action::Follow => {
129131
*uri = location;

src/client/layer/redirect/policy.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Tools for customizing the behavior of a [`FollowRedirect`][super::FollowRedirect] middleware.
22
3-
use http::{Extensions, Request, StatusCode, Uri};
3+
use http::{Extensions, HeaderMap, Request, StatusCode, Uri};
44

55
/// Trait for the policy on handling redirection responses.
66
pub trait Policy<B, E> {
@@ -83,9 +83,10 @@ where
8383

8484
/// A type that holds information on a redirection attempt.
8585
pub struct Attempt<'a> {
86-
pub(crate) status: StatusCode,
87-
pub(crate) location: &'a Uri,
88-
pub(crate) previous: &'a Uri,
86+
pub(super) status: StatusCode,
87+
pub(super) headers: &'a HeaderMap,
88+
pub(super) location: &'a Uri,
89+
pub(super) previous: &'a Uri,
8990
}
9091

9192
impl<'a> Attempt<'a> {
@@ -95,6 +96,12 @@ impl<'a> Attempt<'a> {
9596
self.status
9697
}
9798

99+
/// Returns the headers of the redirection response.
100+
#[inline(always)]
101+
pub fn headers(&self) -> &'a HeaderMap {
102+
self.headers
103+
}
104+
98105
/// Returns the destination URI of the redirection.
99106
#[inline(always)]
100107
pub fn location(&self) -> &'a Uri {

src/redirect.rs

Lines changed: 62 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
77
use std::{error::Error as StdError, fmt, sync::Arc};
88

9-
use http::{Extensions, HeaderMap, HeaderValue, StatusCode, Uri};
9+
use bytes::Bytes;
10+
use http::{Extensions, HeaderMap, HeaderValue, StatusCode, Uri, uri::Authority};
1011

1112
use crate::{
1213
client::{
@@ -28,7 +29,7 @@ use crate::{
2829
/// redirect hops in a chain.
2930
/// - `none` can be used to disable all redirect behavior.
3031
/// - `custom` can be used to create a customized policy.
31-
#[derive(Clone)]
32+
#[derive(Debug, Clone)]
3233
pub struct Policy {
3334
inner: PolicyKind,
3435
}
@@ -38,6 +39,7 @@ pub struct Policy {
3839
#[derive(Debug)]
3940
pub struct Attempt<'a> {
4041
status: StatusCode,
42+
headers: &'a HeaderMap,
4143
next: &'a Uri,
4244
previous: &'a [Uri],
4345
}
@@ -146,9 +148,16 @@ impl Policy {
146148
}
147149
}
148150

149-
pub(crate) fn check(&self, status: StatusCode, next: &Uri, previous: &[Uri]) -> ActionKind {
151+
fn check(
152+
&self,
153+
status: StatusCode,
154+
headers: &HeaderMap,
155+
next: &Uri,
156+
previous: &[Uri],
157+
) -> ActionKind {
150158
self.redirect(Attempt {
151159
status,
160+
headers,
152161
next,
153162
previous,
154163
})
@@ -169,6 +178,11 @@ impl<'a> Attempt<'a> {
169178
self.status
170179
}
171180

181+
/// Get the headers of redirect.
182+
pub fn headers(&self) -> &HeaderMap {
183+
self.headers
184+
}
185+
172186
/// Get the next URI to redirect to.
173187
pub fn uri(&self) -> &Uri {
174188
self.next
@@ -212,12 +226,6 @@ enum PolicyKind {
212226
None,
213227
}
214228

215-
impl fmt::Debug for Policy {
216-
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
217-
f.debug_tuple("Policy").field(&self.inner).finish()
218-
}
219-
}
220-
221229
impl fmt::Debug for PolicyKind {
222230
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
223231
match *self {
@@ -235,21 +243,6 @@ pub(crate) enum ActionKind {
235243
Error(BoxError),
236244
}
237245

238-
fn remove_sensitive_headers(headers: &mut HeaderMap, next: &Uri, previous: &[Uri]) {
239-
if let Some(previous) = previous.last() {
240-
let cross_host = next.host() != previous.host()
241-
|| next.port() != previous.port()
242-
|| next.scheme() != previous.scheme();
243-
if cross_host {
244-
headers.remove(AUTHORIZATION);
245-
headers.remove(COOKIE);
246-
headers.remove("cookie2");
247-
headers.remove(PROXY_AUTHORIZATION);
248-
headers.remove(WWW_AUTHENTICATE);
249-
}
250-
}
251-
}
252-
253246
#[derive(Debug)]
254247
struct TooManyRedirects;
255248

@@ -279,37 +272,17 @@ impl FollowRedirectPolicy {
279272
}
280273
}
281274

282-
pub(crate) fn with_referer(mut self, referer: bool) -> Self {
275+
pub(crate) const fn with_referer(mut self, referer: bool) -> Self {
283276
self.referer = referer;
284277
self
285278
}
286279

287-
pub(crate) fn with_https_only(mut self, https_only: bool) -> Self {
280+
pub(crate) const fn with_https_only(mut self, https_only: bool) -> Self {
288281
self.https_only = https_only;
289282
self
290283
}
291284
}
292285

293-
fn make_referer(next: &Uri, previous: &Uri) -> Option<HeaderValue> {
294-
if next.is_http() && previous.is_https() {
295-
return None;
296-
}
297-
298-
let mut parts = previous.clone().into_parts();
299-
if let Some(authority) = &mut parts.authority {
300-
let host_port = authority.host();
301-
let port = authority.port();
302-
let new_authority = match port {
303-
Some(port) => format!("{}:{}", host_port, port),
304-
None => host_port.to_string(),
305-
};
306-
parts.authority = Some(new_authority.parse().ok()?);
307-
}
308-
309-
let referer = Uri::from_parts(parts).ok()?;
310-
referer.to_string().parse().ok()
311-
}
312-
313286
impl policy::Policy<Body, BoxError> for FollowRedirectPolicy {
314287
fn redirect(&mut self, attempt: &policy::Attempt<'_>) -> Result<policy::Action, BoxError> {
315288
// Parse the next URI from the attempt.
@@ -326,7 +299,7 @@ impl policy::Policy<Body, BoxError> for FollowRedirectPolicy {
326299
.expect("FollowRedirectPolicy should always have a policy set");
327300

328301
// Check if the next URI is already in the list of URLs.
329-
match policy.check(attempt.status(), next_uri, &self.uris) {
302+
match policy.check(attempt.status(), attempt.headers(), next_uri, &self.uris) {
330303
ActionKind::Follow => {
331304
// Validate the next URI's scheme.
332305
if !next_uri.is_http() && !next_uri.is_https() {
@@ -378,6 +351,43 @@ impl policy::Policy<Body, BoxError> for FollowRedirectPolicy {
378351
}
379352
}
380353

354+
fn make_referer(next: &Uri, previous: &Uri) -> Option<HeaderValue> {
355+
if next.is_http() && previous.is_https() {
356+
return None;
357+
}
358+
359+
let referer = {
360+
let mut parts = previous.clone().into_parts();
361+
if let Some(authority) = &mut parts.authority {
362+
let host = authority.host();
363+
parts.authority = match authority.port() {
364+
Some(port) => {
365+
Authority::from_maybe_shared(Bytes::from(format!("{host}:{port}"))).ok()
366+
}
367+
None => host.parse().ok(),
368+
};
369+
}
370+
Uri::from_parts(parts).ok()?
371+
};
372+
373+
HeaderValue::from_maybe_shared(Bytes::from(referer.to_string())).ok()
374+
}
375+
376+
fn remove_sensitive_headers(headers: &mut HeaderMap, next: &Uri, previous: &[Uri]) {
377+
if let Some(previous) = previous.last() {
378+
let cross_host = next.host() != previous.host()
379+
|| next.port() != previous.port()
380+
|| next.scheme() != previous.scheme();
381+
if cross_host {
382+
headers.remove(AUTHORIZATION);
383+
headers.remove(COOKIE);
384+
headers.remove("cookie2");
385+
headers.remove(PROXY_AUTHORIZATION);
386+
headers.remove(WWW_AUTHENTICATE);
387+
}
388+
}
389+
}
390+
381391
#[cfg(test)]
382392
mod tests {
383393
use super::*;
@@ -390,14 +400,14 @@ mod tests {
390400
.map(|i| Uri::try_from(&format!("http://a.b/c/{i}")).unwrap())
391401
.collect::<Vec<_>>();
392402

393-
match policy.check(StatusCode::FOUND, &next, &previous) {
403+
match policy.check(StatusCode::FOUND, &HeaderMap::new(), &next, &previous) {
394404
ActionKind::Follow => (),
395405
other => panic!("unexpected {other:?}"),
396406
}
397407

398408
previous.push(Uri::try_from("http://a.b.d/e/33").unwrap());
399409

400-
match policy.check(StatusCode::FOUND, &next, &previous) {
410+
match policy.check(StatusCode::FOUND, &HeaderMap::new(), &next, &previous) {
401411
ActionKind::Error(err) if err.is::<TooManyRedirects>() => (),
402412
other => panic!("unexpected {other:?}"),
403413
}
@@ -409,7 +419,7 @@ mod tests {
409419
let next = Uri::try_from("http://x.y/z").unwrap();
410420
let previous = vec![Uri::try_from("http://a.b/c").unwrap()];
411421

412-
match policy.check(StatusCode::FOUND, &next, &previous) {
422+
match policy.check(StatusCode::FOUND, &HeaderMap::new(), &next, &previous) {
413423
ActionKind::Error(err) if err.is::<TooManyRedirects>() => (),
414424
other => panic!("unexpected {other:?}"),
415425
}
@@ -426,13 +436,13 @@ mod tests {
426436
});
427437

428438
let next = Uri::try_from("http://bar/baz").unwrap();
429-
match policy.check(StatusCode::FOUND, &next, &[]) {
439+
match policy.check(StatusCode::FOUND, &HeaderMap::new(), &next, &[]) {
430440
ActionKind::Follow => (),
431441
other => panic!("unexpected {other:?}"),
432442
}
433443

434444
let next = Uri::try_from("http://foo/baz").unwrap();
435-
match policy.check(StatusCode::FOUND, &next, &[]) {
445+
match policy.check(StatusCode::FOUND, &HeaderMap::new(), &next, &[]) {
436446
ActionKind::Stop => (),
437447
other => panic!("unexpected {other:?}"),
438448
}

0 commit comments

Comments
 (0)