use http::{HeaderMap, StatusCode};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BodyVerdict {
Success,
ProxyBlocked,
TargetError,
}
pub trait BodyClassifier: Send + Sync + 'static {
fn classify(&self, status: StatusCode, headers: &HeaderMap, body: &[u8]) -> BodyVerdict;
}
pub struct DefaultClassifier;
impl BodyClassifier for DefaultClassifier {
fn classify(&self, status: StatusCode, _headers: &HeaderMap, body: &[u8]) -> BodyVerdict {
if status.is_success() {
if body.is_empty() {
BodyVerdict::ProxyBlocked
} else {
BodyVerdict::Success
}
} else if status == StatusCode::FORBIDDEN || status == StatusCode::TOO_MANY_REQUESTS {
BodyVerdict::ProxyBlocked
} else if status.is_server_error() {
BodyVerdict::TargetError
} else {
BodyVerdict::ProxyBlocked
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn classify(status: u16, body: &[u8]) -> BodyVerdict {
let classifier = DefaultClassifier;
let headers = HeaderMap::new();
classifier.classify(StatusCode::from_u16(status).unwrap(), &headers, body)
}
#[test]
fn success_200_with_body() {
assert_eq!(classify(200, b"hello"), BodyVerdict::Success);
}
#[test]
fn success_201_with_body() {
assert_eq!(classify(201, b"{\"id\":1}"), BodyVerdict::Success);
}
#[test]
fn success_204_no_content_empty_body() {
assert_eq!(classify(204, b""), BodyVerdict::ProxyBlocked);
}
#[test]
fn proxy_blocked_200_empty_body() {
assert_eq!(classify(200, b""), BodyVerdict::ProxyBlocked);
}
#[test]
fn proxy_blocked_403() {
assert_eq!(classify(403, b"Forbidden"), BodyVerdict::ProxyBlocked);
}
#[test]
fn proxy_blocked_429() {
assert_eq!(classify(429, b"Rate limited"), BodyVerdict::ProxyBlocked);
}
#[test]
fn target_error_500() {
assert_eq!(
classify(500, b"Internal Server Error"),
BodyVerdict::TargetError
);
}
#[test]
fn target_error_502() {
assert_eq!(classify(502, b"Bad Gateway"), BodyVerdict::TargetError);
}
#[test]
fn target_error_503() {
assert_eq!(
classify(503, b"Service Unavailable"),
BodyVerdict::TargetError
);
}
#[test]
fn proxy_blocked_301_redirect() {
assert_eq!(classify(301, b"Moved"), BodyVerdict::ProxyBlocked);
}
#[test]
fn proxy_blocked_400_bad_request() {
assert_eq!(classify(400, b"Bad Request"), BodyVerdict::ProxyBlocked);
}
#[test]
fn proxy_blocked_401_unauthorised() {
assert_eq!(classify(401, b"Unauthorised"), BodyVerdict::ProxyBlocked);
}
#[test]
fn proxy_blocked_404_not_found() {
assert_eq!(classify(404, b"Not Found"), BodyVerdict::ProxyBlocked);
}
#[test]
fn proxy_blocked_407_proxy_auth_required() {
assert_eq!(
classify(407, b"Proxy Authentication Required"),
BodyVerdict::ProxyBlocked
);
}
#[test]
fn headers_are_available_to_classifier() {
let classifier = DefaultClassifier;
let mut headers = HeaderMap::new();
headers.insert("x-custom", "value".parse().unwrap());
assert_eq!(
classifier.classify(StatusCode::OK, &headers, b"data"),
BodyVerdict::Success,
);
}
#[test]
fn can_be_used_as_trait_object() {
let classifier: Box<dyn BodyClassifier> = Box::new(DefaultClassifier);
let headers = HeaderMap::new();
assert_eq!(
classifier.classify(StatusCode::OK, &headers, b"body"),
BodyVerdict::Success,
);
}
struct AlwaysSuccess;
impl BodyClassifier for AlwaysSuccess {
fn classify(&self, _status: StatusCode, _headers: &HeaderMap, _body: &[u8]) -> BodyVerdict {
BodyVerdict::Success
}
}
#[test]
fn custom_classifier_overrides_defaults() {
let classifier = AlwaysSuccess;
let headers = HeaderMap::new();
assert_eq!(
classifier.classify(StatusCode::INTERNAL_SERVER_ERROR, &headers, b""),
BodyVerdict::Success,
);
}
#[test]
fn verdict_is_copy_and_clone() {
let v = BodyVerdict::Success;
let v2 = v; let v3 = v; assert_eq!(v, v2);
assert_eq!(v2, v3);
}
#[test]
fn verdict_debug_format() {
assert_eq!(format!("{:?}", BodyVerdict::Success), "Success");
assert_eq!(format!("{:?}", BodyVerdict::ProxyBlocked), "ProxyBlocked");
assert_eq!(format!("{:?}", BodyVerdict::TargetError), "TargetError");
}
}