use async_trait::async_trait;
use bytes::Bytes;
use praxis_core::config::InsecureOptions;
pub(crate) use crate::context::HttpFilterContext;
use crate::{
actions::{FilterAction, SelectedUpstreamBodyOutcome},
body::{BodyAccess, BodyMode},
builtins::http::payload_processing::compression_config::CompressionConfig,
pipeline::FilterPipeline,
};
pub type Filter = dyn HttpFilter;
pub type FilterContext<'a> = HttpFilterContext<'a>;
#[async_trait]
pub trait HttpFilter: Send + Sync {
fn name(&self) -> &'static str;
async fn on_request(&self, ctx: &mut HttpFilterContext<'_>) -> Result<FilterAction, FilterError>;
fn selects_cluster(&self) -> bool {
false
}
fn selected_clusters(&self) -> Vec<String> {
Vec::new()
}
fn load_balancer_clusters(&self) -> Vec<String> {
Vec::new()
}
fn may_select_streaming_subrequest_response(&self) -> bool {
false
}
fn produces_terminal_response(&self) -> bool {
false
}
#[doc(hidden)]
fn visit_nested_pipelines(&mut self, _visitor: &mut dyn FnMut(&mut FilterPipeline)) {}
async fn on_response(&self, ctx: &mut HttpFilterContext<'_>) -> Result<FilterAction, FilterError> {
let _ = ctx;
Ok(FilterAction::Continue)
}
fn request_body_access(&self) -> BodyAccess {
BodyAccess::None
}
fn response_body_access(&self) -> BodyAccess {
BodyAccess::None
}
fn selected_upstream_request_body_access(&self) -> BodyAccess {
BodyAccess::None
}
fn request_body_mode(&self) -> BodyMode {
BodyMode::Stream
}
fn response_body_mode(&self) -> BodyMode {
BodyMode::Stream
}
fn needs_request_context(&self) -> bool {
false
}
fn referenced_files(&self) -> Vec<std::path::PathBuf> {
Vec::new()
}
fn apply_insecure_options(&self, _options: &InsecureOptions) {}
fn compression_config(&self) -> Option<&CompressionConfig> {
None
}
async fn on_request_body(
&self,
ctx: &mut HttpFilterContext<'_>,
body: &mut Option<Bytes>,
end_of_stream: bool,
) -> Result<FilterAction, FilterError> {
let _ = (ctx, body, end_of_stream);
Ok(FilterAction::Continue)
}
fn on_response_body(
&self,
ctx: &mut HttpFilterContext<'_>,
body: &mut Option<Bytes>,
end_of_stream: bool,
) -> Result<FilterAction, FilterError> {
let _ = (ctx, body, end_of_stream);
Ok(FilterAction::Continue)
}
async fn on_selected_upstream_request_body(
&self,
ctx: &mut HttpFilterContext<'_>,
body: &mut Option<Bytes>,
) -> Result<SelectedUpstreamBodyOutcome, FilterError> {
let _ = (ctx, body);
Ok(SelectedUpstreamBodyOutcome::Continue)
}
}
pub type FilterError = Box<dyn std::error::Error + Send + Sync>;
#[cfg(test)]
#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::panic,
reason = "tests"
)]
mod tests {
use async_trait::async_trait;
use super::*;
#[tokio::test]
async fn default_on_response_returns_continue() {
let filter = MinimalFilter;
let req = crate::test_utils::make_request(http::Method::GET, "/");
let mut ctx = crate::test_utils::make_filter_context(&req);
let action = filter.on_response(&mut ctx).await.unwrap();
assert!(
matches!(action, FilterAction::Continue),
"default on_response should return Continue"
);
}
#[test]
fn default_body_access_is_none() {
let filter = MinimalFilter;
assert_eq!(
filter.request_body_access(),
BodyAccess::None,
"default request body access should be None"
);
assert_eq!(
filter.response_body_access(),
BodyAccess::None,
"default response body access should be None"
);
assert_eq!(
filter.selected_upstream_request_body_access(),
BodyAccess::None,
"default selected-upstream request body access should be None"
);
assert_eq!(
filter.request_body_mode(),
BodyMode::Stream,
"default request body mode should be Stream"
);
assert_eq!(
filter.response_body_mode(),
BodyMode::Stream,
"default response body mode should be Stream"
);
assert!(
!filter.needs_request_context(),
"default needs_request_context should be false"
);
}
#[test]
fn default_referenced_files_is_empty() {
let filter = MinimalFilter;
assert!(
filter.referenced_files().is_empty(),
"a filter with no external config must declare nothing"
);
}
#[test]
fn default_cluster_capabilities_are_empty() {
let filter = MinimalFilter;
assert!(!filter.selects_cluster(), "default selects_cluster should be false");
assert!(
filter.selected_clusters().is_empty(),
"default selected_clusters should be empty"
);
assert!(
filter.load_balancer_clusters().is_empty(),
"default load_balancer_clusters should be empty"
);
}
#[tokio::test]
async fn default_body_hooks_return_continue() {
let filter = MinimalFilter;
let req = crate::test_utils::make_request(http::Method::POST, "/");
let mut ctx = crate::test_utils::make_filter_context(&req);
let mut body = Some(Bytes::from_static(b"chunk"));
let request_action = filter.on_request_body(&mut ctx, &mut body, true).await.unwrap();
assert!(
matches!(request_action, FilterAction::Continue),
"default on_request_body should return Continue"
);
let response_action = filter.on_response_body(&mut ctx, &mut body, true).unwrap();
assert!(
matches!(response_action, FilterAction::Continue),
"default on_response_body should return Continue"
);
let selected_action = filter
.on_selected_upstream_request_body(&mut ctx, &mut body)
.await
.unwrap();
assert!(
matches!(selected_action, SelectedUpstreamBodyOutcome::Continue),
"default on_selected_upstream_request_body should return Continue"
);
assert_eq!(
body.as_deref(),
Some(b"chunk".as_slice()),
"defaults must not touch the body"
);
}
#[test]
fn default_streaming_selection_is_disabled() {
let filter = MinimalFilter;
assert!(
!filter.may_select_streaming_subrequest_response(),
"filters must opt in to streaming selection"
);
}
struct MinimalFilter;
#[async_trait]
impl HttpFilter for MinimalFilter {
fn name(&self) -> &'static str {
"minimal"
}
async fn on_request(&self, _ctx: &mut HttpFilterContext<'_>) -> Result<FilterAction, FilterError> {
Ok(FilterAction::Continue)
}
}
}