Skip to main content

oxihttp_client/
redirect.rs

1//! Redirect handling policies for the HTTP client.
2
3/// Policy controlling how the client handles HTTP redirects.
4#[derive(Debug, Clone)]
5pub enum RedirectPolicy {
6    /// Never follow redirects. The redirect response is returned as-is.
7    None,
8    /// Follow up to `n` redirects before returning an error.
9    Limited(usize),
10    /// Follow all redirects (use with caution).
11    All,
12}
13
14impl RedirectPolicy {
15    /// Returns the maximum number of redirects allowed, or `None` for unlimited.
16    pub fn max_redirects(&self) -> Option<usize> {
17        match self {
18            Self::None => Some(0),
19            Self::Limited(n) => Some(*n),
20            Self::All => None,
21        }
22    }
23
24    /// Returns `true` if redirects are disabled.
25    pub fn is_none(&self) -> bool {
26        matches!(self, Self::None)
27    }
28}
29
30impl Default for RedirectPolicy {
31    /// Default: follow up to 10 redirects.
32    fn default() -> Self {
33        Self::Limited(10)
34    }
35}
36
37/// Returns `true` if the given status code is a redirect that should be followed.
38pub fn is_redirect_status(status: http::StatusCode) -> bool {
39    matches!(status.as_u16(), 301 | 302 | 303 | 307 | 308)
40}
41
42/// Determine the method to use after a redirect.
43///
44/// For 301/302/303, POST is changed to GET (as per browser behavior).
45/// For 307/308, the method is preserved.
46pub fn redirect_method(status: http::StatusCode, original: &http::Method) -> http::Method {
47    match status.as_u16() {
48        301..=303 => {
49            if *original == http::Method::HEAD {
50                http::Method::HEAD
51            } else {
52                http::Method::GET
53            }
54        }
55        // 307 and 308 preserve the method
56        _ => original.clone(),
57    }
58}
59
60/// Determine whether to preserve the body after a redirect.
61///
62/// For 307/308, the body should be resent. For 301/302/303, the body is dropped.
63pub fn should_preserve_body(status: http::StatusCode) -> bool {
64    matches!(status.as_u16(), 307 | 308)
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn test_default_policy() {
73        let policy = RedirectPolicy::default();
74        assert_eq!(policy.max_redirects(), Some(10));
75    }
76
77    #[test]
78    fn test_none_policy() {
79        let policy = RedirectPolicy::None;
80        assert!(policy.is_none());
81        assert_eq!(policy.max_redirects(), Some(0));
82    }
83
84    #[test]
85    fn test_is_redirect_status() {
86        assert!(is_redirect_status(http::StatusCode::MOVED_PERMANENTLY));
87        assert!(is_redirect_status(http::StatusCode::FOUND));
88        assert!(is_redirect_status(http::StatusCode::TEMPORARY_REDIRECT));
89        assert!(is_redirect_status(http::StatusCode::PERMANENT_REDIRECT));
90        assert!(!is_redirect_status(http::StatusCode::OK));
91        assert!(!is_redirect_status(http::StatusCode::NOT_FOUND));
92    }
93
94    #[test]
95    fn test_redirect_method_303() {
96        let method = redirect_method(http::StatusCode::SEE_OTHER, &http::Method::POST);
97        assert_eq!(method, http::Method::GET);
98    }
99
100    #[test]
101    fn test_redirect_method_307_preserves() {
102        let method = redirect_method(http::StatusCode::TEMPORARY_REDIRECT, &http::Method::POST);
103        assert_eq!(method, http::Method::POST);
104    }
105
106    #[test]
107    fn test_redirect_method_head_preserved() {
108        let method = redirect_method(http::StatusCode::FOUND, &http::Method::HEAD);
109        assert_eq!(method, http::Method::HEAD);
110    }
111
112    #[test]
113    fn test_should_preserve_body() {
114        assert!(!should_preserve_body(http::StatusCode::MOVED_PERMANENTLY));
115        assert!(!should_preserve_body(http::StatusCode::FOUND));
116        assert!(should_preserve_body(http::StatusCode::TEMPORARY_REDIRECT));
117        assert!(should_preserve_body(http::StatusCode::PERMANENT_REDIRECT));
118    }
119}