chainrpc_core/
rate_limit_headers.rs1use std::time::Duration;
9
10#[derive(Debug, Clone, Default)]
12pub struct RateLimitInfo {
13 pub limit: Option<u32>,
15 pub remaining: Option<u32>,
17 pub reset_after: Option<Duration>,
19 pub retry_after: Option<Duration>,
21 pub is_rate_limited: bool,
23}
24
25impl RateLimitInfo {
26 pub fn from_headers<'a>(headers: impl Iterator<Item = (&'a str, &'a str)>) -> Self {
31 let mut info = Self::default();
32
33 for (name, value) in headers {
34 let lower = name.to_lowercase();
35 match lower.as_str() {
36 "x-ratelimit-limit" | "x-rate-limit-limit" => {
38 info.limit = value.parse().ok();
39 }
40 "x-ratelimit-remaining" | "x-rate-limit-remaining" => {
41 info.remaining = value.parse().ok();
42 }
43 "x-ratelimit-reset" | "x-rate-limit-reset" => {
44 if let Ok(secs) = value.parse::<u64>() {
45 info.reset_after = Some(Duration::from_secs(secs));
46 }
47 }
48 "x-rate-limit-cu-second" => {
50 info.limit = value.parse().ok();
51 }
52 "x-rate-limit-request-second" => {
53 if info.limit.is_none() {
55 info.limit = value.parse().ok();
56 }
57 }
58 "retry-after" => {
60 info.retry_after = parse_retry_after(value);
61 info.is_rate_limited = true;
62 }
63 _ => {}
64 }
65 }
66
67 info
68 }
69
70 pub fn should_backoff(&self) -> bool {
72 self.is_rate_limited || self.remaining == Some(0)
73 }
74
75 pub fn suggested_wait(&self) -> Option<Duration> {
77 self.retry_after.or(self.reset_after).or_else(|| {
79 if self.should_backoff() {
80 Some(Duration::from_secs(1))
81 } else {
82 None
83 }
84 })
85 }
86}
87
88fn parse_retry_after(value: &str) -> Option<Duration> {
90 if let Ok(secs) = value.parse::<u64>() {
92 return Some(Duration::from_secs(secs));
93 }
94 if let Ok(secs) = value.parse::<f64>() {
96 return Some(Duration::from_secs_f64(secs));
97 }
98 Some(Duration::from_secs(1))
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn standard_headers() {
108 let headers = vec![
109 ("X-RateLimit-Limit", "100"),
110 ("X-RateLimit-Remaining", "42"),
111 ("X-RateLimit-Reset", "30"),
112 ];
113 let info = RateLimitInfo::from_headers(headers.iter().map(|(k, v)| (*k, *v)));
114
115 assert_eq!(info.limit, Some(100));
116 assert_eq!(info.remaining, Some(42));
117 assert_eq!(info.reset_after, Some(Duration::from_secs(30)));
118 assert!(!info.is_rate_limited);
119 assert!(!info.should_backoff());
120 }
121
122 #[test]
123 fn retry_after_seconds() {
124 let headers = vec![("Retry-After", "5")];
125 let info = RateLimitInfo::from_headers(headers.iter().map(|(k, v)| (*k, *v)));
126
127 assert!(info.is_rate_limited);
128 assert_eq!(info.retry_after, Some(Duration::from_secs(5)));
129 assert!(info.should_backoff());
130 }
131
132 #[test]
133 fn remaining_zero_triggers_backoff() {
134 let headers = vec![("X-RateLimit-Remaining", "0")];
135 let info = RateLimitInfo::from_headers(headers.iter().map(|(k, v)| (*k, *v)));
136
137 assert!(info.should_backoff());
138 assert!(info.suggested_wait().is_some());
139 }
140
141 #[test]
142 fn alchemy_cu_headers() {
143 let headers = vec![("x-rate-limit-cu-second", "330")];
144 let info = RateLimitInfo::from_headers(headers.iter().map(|(k, v)| (*k, *v)));
145
146 assert_eq!(info.limit, Some(330));
147 }
148
149 #[test]
150 fn case_insensitive() {
151 let headers = vec![
152 ("x-ratelimit-limit", "200"),
153 ("X-RATELIMIT-REMAINING", "50"),
154 ];
155 let info = RateLimitInfo::from_headers(headers.iter().map(|(k, v)| (*k, *v)));
156
157 assert_eq!(info.limit, Some(200));
158 assert_eq!(info.remaining, Some(50));
159 }
160
161 #[test]
162 fn empty_headers() {
163 let info = RateLimitInfo::from_headers(std::iter::empty());
164 assert!(!info.should_backoff());
165 assert!(info.suggested_wait().is_none());
166 }
167
168 #[test]
169 fn suggested_wait_prefers_retry_after() {
170 let headers = vec![("Retry-After", "10"), ("X-RateLimit-Reset", "30")];
171 let info = RateLimitInfo::from_headers(headers.iter().map(|(k, v)| (*k, *v)));
172
173 assert_eq!(info.suggested_wait(), Some(Duration::from_secs(10)));
174 }
175}