use std::{borrow::Cow, net::IpAddr, sync::Arc, time::Instant};
use http::{HeaderMap, Method, StatusCode, Uri};
use praxis_core::{connectivity::Upstream, health::HealthRegistry};
pub struct HttpFilterContext<'a> {
pub client_addr: Option<IpAddr>,
pub cluster: Option<Arc<str>>,
pub extra_request_headers: Vec<(Cow<'static, str>, String)>,
pub health_registry: Option<&'a HealthRegistry>,
pub request: &'a Request,
pub request_body_bytes: u64,
pub request_start: Instant,
pub response_body_bytes: u64,
pub response_header: Option<&'a mut Response>,
pub response_headers_modified: bool,
pub rewritten_path: Option<String>,
pub upstream: Option<Upstream>,
}
impl HttpFilterContext<'_> {
pub fn cluster_name(&self) -> Option<&str> {
self.cluster.as_deref()
}
pub fn upstream_addr(&self) -> Option<&str> {
self.upstream.as_ref().map(|u| &*u.address)
}
pub fn request_id(&self) -> Option<&str> {
self.request.headers.get("x-request-id").and_then(|v| v.to_str().ok())
}
}
#[derive(Debug, Clone)]
pub struct Request {
pub headers: HeaderMap,
pub method: Method,
pub uri: Uri,
}
#[derive(Debug)]
pub struct Response {
pub headers: HeaderMap,
pub status: StatusCode,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_fields_are_accessible() {
let req = Request {
method: Method::POST,
uri: "/submit".parse().unwrap(),
headers: HeaderMap::new(),
};
assert_eq!(req.method, Method::POST);
assert_eq!(req.uri.path(), "/submit");
assert!(req.headers.is_empty(), "new request should have no headers");
}
#[test]
fn response_header_mutation() {
let mut resp = Response {
status: StatusCode::OK,
headers: HeaderMap::new(),
};
resp.headers.insert("x-powered-by", "praxis".parse().unwrap());
assert_eq!(resp.headers["x-powered-by"], "praxis");
}
#[test]
fn response_status_codes() {
for code in [200u16, 404, 500] {
let resp = Response {
status: StatusCode::from_u16(code).unwrap(),
headers: HeaderMap::new(),
};
assert_eq!(resp.status.as_u16(), code);
}
}
#[test]
fn cluster_name_returns_none_when_unset() {
let req = crate::test_utils::make_request(Method::GET, "/");
let ctx = crate::test_utils::make_filter_context(&req);
assert!(ctx.cluster_name().is_none(), "cluster name should be None when unset");
}
#[test]
fn cluster_name_returns_value_when_set() {
let req = crate::test_utils::make_request(Method::GET, "/");
let mut ctx = crate::test_utils::make_filter_context(&req);
ctx.cluster = Some(Arc::from("backend"));
assert_eq!(
ctx.cluster_name(),
Some("backend"),
"cluster name should return set value"
);
}
#[test]
fn upstream_addr_returns_none_when_unset() {
let req = crate::test_utils::make_request(Method::GET, "/");
let ctx = crate::test_utils::make_filter_context(&req);
assert!(ctx.upstream_addr().is_none(), "upstream addr should be None when unset");
}
#[test]
fn upstream_addr_returns_value_when_set() {
let req = crate::test_utils::make_request(Method::GET, "/");
let mut ctx = crate::test_utils::make_filter_context(&req);
ctx.upstream = Some(Upstream {
address: Arc::from("10.0.0.1:8080"),
tls: None,
connection: Arc::new(praxis_core::connectivity::ConnectionOptions::default()),
});
assert_eq!(
ctx.upstream_addr(),
Some("10.0.0.1:8080"),
"upstream addr should return set address"
);
}
#[test]
fn request_id_returns_none_when_absent() {
let req = crate::test_utils::make_request(Method::GET, "/");
let ctx = crate::test_utils::make_filter_context(&req);
assert!(
ctx.request_id().is_none(),
"request ID should be None when header absent"
);
}
#[test]
fn request_id_returns_value_when_present() {
let mut req = crate::test_utils::make_request(Method::GET, "/");
req.headers.insert("x-request-id", "abc-123".parse().unwrap());
let ctx = crate::test_utils::make_filter_context(&req);
assert_eq!(
ctx.request_id(),
Some("abc-123"),
"request ID should return header value"
);
}
}