scatter_proxy/
classifier.rs1use http::{HeaderMap, StatusCode};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum BodyVerdict {
12 Success,
13 ProxyBlocked,
14 TargetError,
15}
16
17pub trait BodyClassifier: Send + Sync + 'static {
21 fn classify(&self, status: StatusCode, headers: &HeaderMap, body: &[u8]) -> BodyVerdict;
22}
23
24pub struct DefaultClassifier;
34
35impl BodyClassifier for DefaultClassifier {
36 fn classify(&self, status: StatusCode, _headers: &HeaderMap, body: &[u8]) -> BodyVerdict {
37 if status.is_success() {
38 if body.is_empty() {
39 BodyVerdict::ProxyBlocked
40 } else {
41 BodyVerdict::Success
42 }
43 } else if status == StatusCode::FORBIDDEN || status == StatusCode::TOO_MANY_REQUESTS {
44 BodyVerdict::ProxyBlocked
45 } else if status.is_server_error() {
46 BodyVerdict::TargetError
47 } else {
48 BodyVerdict::ProxyBlocked
49 }
50 }
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56
57 fn classify(status: u16, body: &[u8]) -> BodyVerdict {
60 let classifier = DefaultClassifier;
61 let headers = HeaderMap::new();
62 classifier.classify(StatusCode::from_u16(status).unwrap(), &headers, body)
63 }
64
65 #[test]
68 fn success_200_with_body() {
69 assert_eq!(classify(200, b"hello"), BodyVerdict::Success);
70 }
71
72 #[test]
73 fn success_201_with_body() {
74 assert_eq!(classify(201, b"{\"id\":1}"), BodyVerdict::Success);
75 }
76
77 #[test]
78 fn success_204_no_content_empty_body() {
79 assert_eq!(classify(204, b""), BodyVerdict::ProxyBlocked);
81 }
82
83 #[test]
84 fn proxy_blocked_200_empty_body() {
85 assert_eq!(classify(200, b""), BodyVerdict::ProxyBlocked);
86 }
87
88 #[test]
91 fn proxy_blocked_403() {
92 assert_eq!(classify(403, b"Forbidden"), BodyVerdict::ProxyBlocked);
93 }
94
95 #[test]
96 fn proxy_blocked_429() {
97 assert_eq!(classify(429, b"Rate limited"), BodyVerdict::ProxyBlocked);
98 }
99
100 #[test]
103 fn target_error_500() {
104 assert_eq!(
105 classify(500, b"Internal Server Error"),
106 BodyVerdict::TargetError
107 );
108 }
109
110 #[test]
111 fn target_error_502() {
112 assert_eq!(classify(502, b"Bad Gateway"), BodyVerdict::TargetError);
113 }
114
115 #[test]
116 fn target_error_503() {
117 assert_eq!(
118 classify(503, b"Service Unavailable"),
119 BodyVerdict::TargetError
120 );
121 }
122
123 #[test]
126 fn proxy_blocked_301_redirect() {
127 assert_eq!(classify(301, b"Moved"), BodyVerdict::ProxyBlocked);
128 }
129
130 #[test]
131 fn proxy_blocked_400_bad_request() {
132 assert_eq!(classify(400, b"Bad Request"), BodyVerdict::ProxyBlocked);
133 }
134
135 #[test]
136 fn proxy_blocked_401_unauthorised() {
137 assert_eq!(classify(401, b"Unauthorised"), BodyVerdict::ProxyBlocked);
138 }
139
140 #[test]
141 fn proxy_blocked_404_not_found() {
142 assert_eq!(classify(404, b"Not Found"), BodyVerdict::ProxyBlocked);
143 }
144
145 #[test]
146 fn proxy_blocked_407_proxy_auth_required() {
147 assert_eq!(
148 classify(407, b"Proxy Authentication Required"),
149 BodyVerdict::ProxyBlocked
150 );
151 }
152
153 #[test]
156 fn headers_are_available_to_classifier() {
157 let classifier = DefaultClassifier;
158 let mut headers = HeaderMap::new();
159 headers.insert("x-custom", "value".parse().unwrap());
160 assert_eq!(
162 classifier.classify(StatusCode::OK, &headers, b"data"),
163 BodyVerdict::Success,
164 );
165 }
166
167 #[test]
170 fn can_be_used_as_trait_object() {
171 let classifier: Box<dyn BodyClassifier> = Box::new(DefaultClassifier);
172 let headers = HeaderMap::new();
173 assert_eq!(
174 classifier.classify(StatusCode::OK, &headers, b"body"),
175 BodyVerdict::Success,
176 );
177 }
178
179 struct AlwaysSuccess;
182
183 impl BodyClassifier for AlwaysSuccess {
184 fn classify(&self, _status: StatusCode, _headers: &HeaderMap, _body: &[u8]) -> BodyVerdict {
185 BodyVerdict::Success
186 }
187 }
188
189 #[test]
190 fn custom_classifier_overrides_defaults() {
191 let classifier = AlwaysSuccess;
192 let headers = HeaderMap::new();
193 assert_eq!(
195 classifier.classify(StatusCode::INTERNAL_SERVER_ERROR, &headers, b""),
196 BodyVerdict::Success,
197 );
198 }
199
200 #[test]
203 fn verdict_is_copy_and_clone() {
204 let v = BodyVerdict::Success;
205 let v2 = v; let v3 = v; assert_eq!(v, v2);
208 assert_eq!(v2, v3);
209 }
210
211 #[test]
212 fn verdict_debug_format() {
213 assert_eq!(format!("{:?}", BodyVerdict::Success), "Success");
214 assert_eq!(format!("{:?}", BodyVerdict::ProxyBlocked), "ProxyBlocked");
215 assert_eq!(format!("{:?}", BodyVerdict::TargetError), "TargetError");
216 }
217}