gproxy_channel_api/
disposition.rs1use std::time::Duration;
4
5use http::{HeaderMap, StatusCode};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum Disposition {
11 Success,
13 AuthDead,
15 RateLimited { retry_after: Option<Duration> },
18 Transient,
20 Permanent,
22}
23
24impl Disposition {
25 pub fn is_success(&self) -> bool {
26 matches!(self, Self::Success)
27 }
28
29 pub fn should_failover(&self) -> bool {
31 matches!(
32 self,
33 Self::AuthDead | Self::RateLimited { .. } | Self::Transient
34 )
35 }
36
37 pub fn from_http(status: StatusCode, headers: &HeaderMap) -> Self {
41 let code = status.as_u16();
42 match code {
43 200..=299 => Self::Success,
44 401..=403 => Self::AuthDead,
45 429 => Self::RateLimited {
46 retry_after: parse_retry_after(headers),
47 },
48 500..=599 => Self::Transient,
49 _ => Self::Permanent,
50 }
51 }
52}
53
54fn parse_retry_after(headers: &HeaderMap) -> Option<Duration> {
59 let val = headers
60 .get(http::header::RETRY_AFTER)?
61 .to_str()
62 .ok()?
63 .trim();
64
65 if let Ok(secs) = val.parse::<u64>() {
67 return Some(Duration::from_secs(secs));
68 }
69
70 let target = httpdate::parse_http_date(val)
73 .ok()?
74 .duration_since(std::time::UNIX_EPOCH)
75 .ok()?
76 .as_secs() as i64;
77 let now = unix_now();
78 (target > now).then(|| Duration::from_secs((target - now) as u64))
79}
80
81#[cfg(not(target_arch = "wasm32"))]
82fn unix_now() -> i64 {
83 std::time::SystemTime::now()
84 .duration_since(std::time::UNIX_EPOCH)
85 .unwrap_or_default()
86 .as_secs() as i64
87}
88
89#[cfg(target_arch = "wasm32")]
90fn unix_now() -> i64 {
91 (js_sys::Date::now() / 1000.0) as i64
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn status_mapping() {
100 let h = HeaderMap::new();
101 assert_eq!(
102 Disposition::from_http(StatusCode::OK, &h),
103 Disposition::Success
104 );
105 assert_eq!(
106 Disposition::from_http(StatusCode::UNAUTHORIZED, &h),
107 Disposition::AuthDead
108 );
109 assert_eq!(
110 Disposition::from_http(StatusCode::BAD_GATEWAY, &h),
111 Disposition::Transient
112 );
113 assert_eq!(
114 Disposition::from_http(StatusCode::BAD_REQUEST, &h),
115 Disposition::Permanent
116 );
117 assert!(Disposition::from_http(StatusCode::OK, &h).is_success());
118 }
119
120 #[test]
121 fn retry_after_parsed() {
122 let mut h = HeaderMap::new();
123 h.insert(http::header::RETRY_AFTER, "12".parse().unwrap());
124 assert_eq!(
125 Disposition::from_http(StatusCode::TOO_MANY_REQUESTS, &h),
126 Disposition::RateLimited {
127 retry_after: Some(Duration::from_secs(12))
128 }
129 );
130 }
131
132 #[test]
133 fn retry_after_http_date_form() {
134 let mut h = HeaderMap::new();
136 h.insert(
137 http::header::RETRY_AFTER,
138 "Wed, 21 Oct 2099 07:28:00 GMT".parse().unwrap(),
139 );
140 match Disposition::from_http(StatusCode::TOO_MANY_REQUESTS, &h) {
141 Disposition::RateLimited {
142 retry_after: Some(d),
143 } => assert!(d.as_secs() > 0, "future date → positive delay"),
144 other => panic!("expected RateLimited with delay, got {other:?}"),
145 }
146
147 let mut past = HeaderMap::new();
148 past.insert(
149 http::header::RETRY_AFTER,
150 "Wed, 21 Oct 1999 07:28:00 GMT".parse().unwrap(),
151 );
152 assert_eq!(
153 Disposition::from_http(StatusCode::TOO_MANY_REQUESTS, &past),
154 Disposition::RateLimited { retry_after: None },
155 "past date → no cooldown"
156 );
157 }
158}