saluki_io/net/util/retry/classifier/
http.rs1use std::sync::Arc;
2
3use http::Response;
4
5use super::RetryClassifier;
6
7pub type HttpRetryPredicate<B = ()> = Arc<dyn Fn(&Response<B>) -> bool + Send + Sync>;
11
12fn default_should_retry<B>(response: &Response<B>) -> bool {
13 let status = response.status();
14
15 match status {
16 http::StatusCode::BAD_REQUEST
19 | http::StatusCode::UNAUTHORIZED
20 | http::StatusCode::FORBIDDEN
21 | http::StatusCode::PAYLOAD_TOO_LARGE => {
22 saluki_antithesis::sometimes!(
25 true,
26 "transaction permanently dropped — non-retryable status",
27 { "status": status.as_u16() }
28 );
29 false
30 }
31
32 _ => status.is_client_error() || status.is_server_error(),
34 }
35}
36
37pub struct StandardHttpClassifier<B = ()> {
52 predicates: Vec<HttpRetryPredicate<B>>,
53}
54
55impl<B> Clone for StandardHttpClassifier<B> {
56 fn clone(&self) -> Self {
57 Self {
58 predicates: self.predicates.clone(),
59 }
60 }
61}
62
63impl<B: 'static> Default for StandardHttpClassifier<B> {
64 fn default() -> Self {
65 Self::new()
66 }
67}
68
69impl<B: 'static> StandardHttpClassifier<B> {
70 pub fn new() -> Self {
72 Self {
73 predicates: vec![Arc::new(default_should_retry::<B>)],
74 }
75 }
76
77 pub fn with_predicate(mut self, predicate: HttpRetryPredicate<B>) -> Self {
81 self.predicates.push(predicate);
82 self
83 }
84}
85
86impl<B, Error> RetryClassifier<http::Response<B>, Error> for StandardHttpClassifier<B> {
87 fn should_retry(&self, response: &Result<http::Response<B>, Error>) -> bool {
88 match response {
89 Ok(resp) => self.predicates.iter().any(|p| p(resp)),
90 Err(_) => true,
91 }
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use std::sync::atomic::{AtomicBool, Ordering};
98
99 use http::StatusCode;
100
101 use super::*;
102
103 type TestResponse = Result<http::Response<()>, ()>;
104
105 fn ok(status: StatusCode) -> TestResponse {
106 Ok(http::Response::builder().status(status).body(()).unwrap())
107 }
108
109 fn err() -> TestResponse {
110 Err(())
111 }
112
113 fn classify(classifier: &StandardHttpClassifier<()>, response: &TestResponse) -> bool {
114 <StandardHttpClassifier<()> as RetryClassifier<http::Response<()>, ()>>::should_retry(classifier, response)
115 }
116
117 #[test]
118 fn default_classifier_retries_5xx_and_most_4xx() {
119 let classifier = StandardHttpClassifier::new();
120
121 assert!(!classify(&classifier, &ok(StatusCode::OK)));
122 assert!(!classify(&classifier, &ok(StatusCode::NO_CONTENT)));
123
124 for status in [
125 StatusCode::INTERNAL_SERVER_ERROR,
126 StatusCode::BAD_GATEWAY,
127 StatusCode::SERVICE_UNAVAILABLE,
128 StatusCode::GATEWAY_TIMEOUT,
129 ] {
130 assert!(classify(&classifier, &ok(status)), "{} should be retried", status);
131 }
132
133 for status in [
134 StatusCode::REQUEST_TIMEOUT,
135 StatusCode::TOO_MANY_REQUESTS,
136 StatusCode::NOT_FOUND,
137 ] {
138 assert!(classify(&classifier, &ok(status)), "{} should be retried", status);
139 }
140 }
141
142 #[test]
143 fn default_classifier_does_not_retry_known_client_misconfig() {
144 let classifier = StandardHttpClassifier::new();
145
146 for status in [
147 StatusCode::BAD_REQUEST,
148 StatusCode::UNAUTHORIZED,
149 StatusCode::FORBIDDEN,
150 StatusCode::PAYLOAD_TOO_LARGE,
151 ] {
152 assert!(!classify(&classifier, &ok(status)), "{} should not be retried", status);
153 }
154 }
155
156 #[test]
157 fn default_classifier_retries_transport_error() {
158 let classifier = StandardHttpClassifier::new();
159 assert!(classify(&classifier, &err()));
160 }
161
162 #[test]
163 fn predicate_adds_retry_for_403() {
164 let classifier = StandardHttpClassifier::new()
165 .with_predicate(Arc::new(|response| response.status() == StatusCode::FORBIDDEN));
166
167 assert!(classify(&classifier, &ok(StatusCode::FORBIDDEN)));
168 assert!(!classify(&classifier, &ok(StatusCode::UNAUTHORIZED)));
170 assert!(!classify(&classifier, &ok(StatusCode::BAD_REQUEST)));
171 assert!(classify(&classifier, &ok(StatusCode::INTERNAL_SERVER_ERROR)));
173 }
174
175 #[test]
176 fn predicate_is_re_evaluated_each_call() {
177 let flag = Arc::new(AtomicBool::new(false));
178 let flag_clone = Arc::clone(&flag);
179 let predicate: HttpRetryPredicate =
180 Arc::new(move |response| response.status() == StatusCode::FORBIDDEN && flag_clone.load(Ordering::SeqCst));
181
182 let classifier = StandardHttpClassifier::new().with_predicate(predicate);
183
184 assert!(!classify(&classifier, &ok(StatusCode::FORBIDDEN)));
185
186 flag.store(true, Ordering::SeqCst);
187 assert!(classify(&classifier, &ok(StatusCode::FORBIDDEN)));
188
189 flag.store(false, Ordering::SeqCst);
190 assert!(!classify(&classifier, &ok(StatusCode::FORBIDDEN)));
191 }
192}