use bytes::Bytes;
use praxis_core::config::FailureMode;
use tracing::{Instrument as _, debug, debug_span, trace, warn};
use super::{check_failure_mode, filter::PipelineFilter};
use crate::{
FilterError,
actions::{FilterAction, Rejection, SelectedUpstreamBodyOutcome},
any_filter::AnyFilter,
body::BodyAccess,
condition::{SelectedUpstream, should_execute_from, should_execute_response_ref},
context::{EffectiveHeaders, HttpFilterContext, Response},
metrics::{
PHASE_REQUEST, PHASE_RESPONSE, PHASE_SELECTED_UPSTREAM, STREAM_BODY, STREAM_HEADERS, record_filter_duration,
},
};
#[cfg(feature = "bound-upstream-request-body")]
use crate::{actions::BoundUpstreamBodyOutcome, metrics::PHASE_BOUND_UPSTREAM};
pub(super) fn accumulate_body_bytes(counter: &mut u64, body: Option<&Bytes>) {
if let Some(b) = body {
*counter += b.len() as u64;
}
}
pub(super) fn released_or_continue(released: bool) -> FilterAction {
if released {
FilterAction::Release
} else {
FilterAction::Continue
}
}
pub(super) fn ctx_selected_upstream<'a>(ctx: &'a HttpFilterContext<'_>) -> SelectedUpstream<'a> {
SelectedUpstream {
application_protocol: ctx.selected_application_protocol(),
application_provider: ctx.selected_application_provider(),
}
}
pub(super) fn as_request_body_filter<'a>(
pf: &'a PipelineFilter,
ctx: &HttpFilterContext<'_>,
conditions_resolved: bool,
) -> Result<Option<&'a dyn crate::filter::HttpFilter>, FilterError> {
let AnyFilter::Http(http_filter) = &pf.filter else {
return Ok(None);
};
if !conditions_resolved && !pf.conditions.is_empty() {
let run = should_execute_from(
&pf.conditions,
ctx.request,
&EffectiveHeaders(ctx),
ctx.bound_upstream_view(),
ctx_selected_upstream(ctx),
)
.map_err(|e| FilterError::from(format!("{}: {e}", http_filter.name())))?;
if !run {
debug!(
filter = http_filter.name(),
"body hook skipped by conditions (effective headers)"
);
return Ok(None);
}
}
Ok(Some(http_filter.as_ref()))
}
pub(super) fn as_response_body_filter<'a>(
filter: &'a AnyFilter,
resp_conditions: &[praxis_core::config::ResponseCondition],
response_header: Option<&Response>,
) -> Option<&'a dyn crate::filter::HttpFilter> {
let http_filter = match filter {
AnyFilter::Http(f) => f.as_ref(),
AnyFilter::Tcp(_) => return None,
};
if skip_by_response_conditions_with_header(http_filter, resp_conditions, response_header) {
return None;
}
Some(http_filter)
}
#[derive(Debug)]
pub(super) enum BodyFilterOutcome {
BodyDone,
Continue,
Released,
Rejected(Rejection),
}
pub(super) fn dispatch_body_result(
result: Result<FilterAction, FilterError>,
filter_name: &str,
phase: &str,
failure_mode: FailureMode,
) -> Result<BodyFilterOutcome, FilterError> {
match result {
Ok(FilterAction::Continue) => Ok(BodyFilterOutcome::Continue),
Ok(FilterAction::TerminalResponse(_) | FilterAction::StreamingTerminalResponse(_)) => {
warn!(
filter = filter_name,
"{phase}: terminal response ignored; only request-phase filters may synthesize a response"
);
Ok(BodyFilterOutcome::Continue)
},
Ok(FilterAction::Release) => {
debug!(filter = filter_name, "filter released body");
Ok(BodyFilterOutcome::Released)
},
Ok(FilterAction::Reject(rejection)) => {
warn!(
filter = filter_name,
status = rejection.status,
"{phase} rejected by filter"
);
Ok(BodyFilterOutcome::Rejected(rejection))
},
Ok(FilterAction::BodyDone) => {
debug!(filter = filter_name, "filter signaled body done");
Ok(BodyFilterOutcome::BodyDone)
},
Err(e) => {
check_failure_mode(filter_name, e, phase, failure_mode)?;
Ok(BodyFilterOutcome::Continue)
},
}
}
pub(super) fn dispatch_selected_upstream_body_result(
result: Result<SelectedUpstreamBodyOutcome, FilterError>,
filter_name: &str,
failure_mode: FailureMode,
) -> Result<SelectedUpstreamBodyOutcome, FilterError> {
match result {
Ok(SelectedUpstreamBodyOutcome::Continue) => Ok(SelectedUpstreamBodyOutcome::Continue),
Ok(SelectedUpstreamBodyOutcome::Reject(rejection)) => {
warn!(
filter = filter_name,
status = rejection.status,
"selected-upstream request body rejected by filter"
);
Ok(SelectedUpstreamBodyOutcome::Reject(rejection))
},
Err(e) => {
check_failure_mode(filter_name, e, "selected-upstream request body", failure_mode)?;
Ok(SelectedUpstreamBodyOutcome::Continue)
},
}
}
fn record_selected_upstream_result(span: &tracing::Span, result: &Result<SelectedUpstreamBodyOutcome, FilterError>) {
let label = match result {
Ok(SelectedUpstreamBodyOutcome::Continue) => "continue",
Ok(SelectedUpstreamBodyOutcome::Reject(_)) => "reject",
Err(_) => "error",
};
span.record("filter.result", label);
}
#[cfg(feature = "bound-upstream-request-body")]
pub(super) fn dispatch_bound_upstream_body_result(
result: Result<BoundUpstreamBodyOutcome, FilterError>,
filter_name: &str,
failure_mode: FailureMode,
) -> Result<BoundUpstreamBodyOutcome, FilterError> {
match result {
Ok(BoundUpstreamBodyOutcome::Continue) => Ok(BoundUpstreamBodyOutcome::Continue),
Ok(BoundUpstreamBodyOutcome::Reject(rejection)) => {
warn!(
filter = filter_name,
status = rejection.status,
"bound-upstream request body rejected by filter"
);
Ok(BoundUpstreamBodyOutcome::Reject(rejection))
},
Err(e) => {
check_failure_mode(filter_name, e, "bound-upstream request body", failure_mode)?;
Ok(BoundUpstreamBodyOutcome::Continue)
},
}
}
#[cfg(feature = "bound-upstream-request-body")]
fn record_bound_upstream_result(span: &tracing::Span, result: &Result<BoundUpstreamBodyOutcome, FilterError>) {
let label = match result {
Ok(BoundUpstreamBodyOutcome::Continue) => "continue",
Ok(BoundUpstreamBodyOutcome::Reject(_)) => "reject",
Err(_) => "error",
};
span.record("filter.result", label);
}
pub(super) fn skip_by_response_conditions(
http_filter: &dyn crate::filter::HttpFilter,
resp_conditions: &[praxis_core::config::ResponseCondition],
ctx: &HttpFilterContext<'_>,
) -> bool {
let response_header = ctx.response_header.as_deref();
skip_by_response_conditions_with_header(http_filter, resp_conditions, response_header)
}
pub(super) fn skip_by_response_conditions_with_header(
http_filter: &dyn crate::filter::HttpFilter,
resp_conditions: &[praxis_core::config::ResponseCondition],
response_header: Option<&Response>,
) -> bool {
let Some(resp) = response_header else {
return false;
};
if !resp_conditions.is_empty() && !should_execute_response_ref(resp_conditions, resp.status, &resp.headers) {
trace!(filter = http_filter.name(), "skipped by response conditions");
return true;
}
false
}
pub(super) enum HeaderFilterOutcome {
Continue,
Rejected(Rejection),
TerminalResponse(Box<crate::actions::TerminalResponse>),
StreamingTerminalResponse(Box<crate::actions::StreamingTerminalResponse>),
}
#[expect(clippy::too_many_lines, reason = "metrics instrumentation adds branches per hook")]
pub(super) async fn run_request_filter(
http_filter: &dyn crate::filter::HttpFilter,
ctx: &mut HttpFilterContext<'_>,
failure_mode: FailureMode,
metrics_enabled: bool,
) -> Result<HeaderFilterOutcome, FilterError> {
let filter_span = debug_span!(
"filter",
"otel.name" = %format_args!("filter:{}:request", http_filter.name()),
"filter.name" = http_filter.name(),
"filter.phase" = "request",
"filter.result" = tracing::field::Empty,
);
let request_result = async {
trace!("on_request");
let result = if metrics_enabled {
let start = std::time::Instant::now();
let result = http_filter.on_request(ctx).await;
record_filter_duration(
http_filter.name(),
PHASE_REQUEST,
STREAM_HEADERS,
start.elapsed().as_secs_f64(),
);
result
} else {
http_filter.on_request(ctx).await
};
record_filter_result(&filter_span, &result);
result
}
.instrument(filter_span.clone())
.await;
match request_result {
Ok(FilterAction::Continue | FilterAction::Release | FilterAction::BodyDone) => {
Ok(HeaderFilterOutcome::Continue)
},
Ok(FilterAction::Reject(rejection)) => {
warn!(
filter = http_filter.name(),
status = rejection.status,
"request rejected by filter"
);
Ok(HeaderFilterOutcome::Rejected(rejection))
},
Ok(FilterAction::TerminalResponse(terminal)) => {
debug!(
filter = http_filter.name(),
status = terminal.status,
"filter produced terminal response"
);
Ok(HeaderFilterOutcome::TerminalResponse(terminal))
},
Ok(FilterAction::StreamingTerminalResponse(terminal)) => {
debug!(
filter = http_filter.name(),
status = terminal.status,
"filter produced streaming terminal response"
);
Ok(HeaderFilterOutcome::StreamingTerminalResponse(terminal))
},
Err(e) => {
check_failure_mode(http_filter.name(), e, "request", failure_mode)?;
Ok(HeaderFilterOutcome::Continue)
},
}
}
#[expect(clippy::too_many_arguments, reason = "metrics_enabled flag is required per hook")]
pub(super) async fn run_request_body_filter(
http_filter: &dyn crate::filter::HttpFilter,
ctx: &mut HttpFilterContext<'_>,
body: &mut Option<Bytes>,
end_of_stream: bool,
failure_mode: FailureMode,
metrics_enabled: bool,
) -> Result<BodyFilterOutcome, FilterError> {
let filter_span = debug_span!(
"filter",
"otel.name" = %format_args!("filter:{}:request_body", http_filter.name()),
"filter.name" = http_filter.name(),
"filter.phase" = "request_body",
"filter.result" = tracing::field::Empty,
);
let body_result = async {
trace!("on_request_body");
let result = if metrics_enabled {
let start = std::time::Instant::now();
let result = http_filter.on_request_body(ctx, body, end_of_stream).await;
record_filter_duration(
http_filter.name(),
PHASE_REQUEST,
STREAM_BODY,
start.elapsed().as_secs_f64(),
);
result
} else {
http_filter.on_request_body(ctx, body, end_of_stream).await
};
record_filter_result(&filter_span, &result);
result
}
.instrument(filter_span.clone())
.await;
dispatch_body_result(body_result, http_filter.name(), "request body", failure_mode)
}
fn body_hook_span(filter_name: &'static str, phase: &'static str) -> tracing::Span {
debug_span!(
"filter",
"otel.name" = %format_args!("filter:{filter_name}:{phase}"),
"filter.name" = filter_name,
"filter.phase" = phase,
"filter.result" = tracing::field::Empty,
)
}
async fn timed_body_hook<T>(
metrics_enabled: bool,
filter_name: &'static str,
phase: &'static str,
hook: impl Future<Output = T>,
) -> T {
if !metrics_enabled {
return hook.await;
}
let start = std::time::Instant::now();
let result = hook.await;
record_filter_duration(filter_name, phase, STREAM_BODY, start.elapsed().as_secs_f64());
result
}
pub(super) async fn run_selected_upstream_request_body_filter(
http_filter: &dyn crate::filter::HttpFilter,
ctx: &mut HttpFilterContext<'_>,
body: &mut Option<Bytes>,
failure_mode: FailureMode,
metrics_enabled: bool,
) -> Result<SelectedUpstreamBodyOutcome, FilterError> {
let filter_span = body_hook_span(http_filter.name(), "selected_upstream_request_body");
let before = body.clone();
let mut scratch = body.clone();
let target = if http_filter.selected_upstream_request_body_access() == BodyAccess::ReadWrite {
&mut *body
} else {
&mut scratch
};
let body_result = async {
trace!("on_selected_upstream_request_body");
let result = timed_body_hook(
metrics_enabled,
http_filter.name(),
PHASE_SELECTED_UPSTREAM,
http_filter.on_selected_upstream_request_body(ctx, target),
)
.await;
record_selected_upstream_result(&filter_span, &result);
result
}
.instrument(filter_span.clone())
.await;
if body_result.is_err() {
*body = before;
}
dispatch_selected_upstream_body_result(body_result, http_filter.name(), failure_mode)
}
#[cfg(feature = "bound-upstream-request-body")]
pub(super) async fn run_bound_upstream_request_body_filter(
http_filter: &dyn crate::filter::HttpFilter,
ctx: &mut HttpFilterContext<'_>,
body: &mut Option<Bytes>,
failure_mode: FailureMode,
metrics_enabled: bool,
) -> Result<(BoundUpstreamBodyOutcome, bool), FilterError> {
let writer = http_filter.bound_upstream_request_body_access() == BodyAccess::ReadWrite;
let before = body.clone();
let mut scratch = body.clone();
let target = if writer { &mut *body } else { &mut scratch };
let filter_span = body_hook_span(http_filter.name(), "bound_upstream_request_body");
let body_result = async {
trace!("on_bound_upstream_request_body");
let result = timed_body_hook(
metrics_enabled,
http_filter.name(),
PHASE_BOUND_UPSTREAM,
http_filter.on_bound_upstream_request_body(ctx, target),
)
.await;
record_bound_upstream_result(&filter_span, &result);
result
}
.instrument(filter_span.clone())
.await;
let rewrote = writer && body_result.is_ok();
if body_result.is_err() {
*body = before;
}
dispatch_bound_upstream_body_result(body_result, http_filter.name(), failure_mode).map(|outcome| (outcome, rewrote))
}
#[expect(clippy::too_many_arguments, reason = "metrics_enabled flag is required per hook")]
pub(super) fn run_response_body_filter(
http_filter: &dyn crate::filter::HttpFilter,
ctx: &mut HttpFilterContext<'_>,
body: &mut Option<Bytes>,
end_of_stream: bool,
failure_mode: FailureMode,
metrics_enabled: bool,
) -> Result<BodyFilterOutcome, FilterError> {
let filter_span = debug_span!(
"filter",
"otel.name" = %format_args!("filter:{}:response_body", http_filter.name()),
"filter.name" = http_filter.name(),
"filter.phase" = "response_body",
"filter.result" = tracing::field::Empty,
);
let _entered = filter_span.enter();
trace!("on_response_body");
let body_result = if metrics_enabled {
let start = std::time::Instant::now();
let result = http_filter.on_response_body(ctx, body, end_of_stream);
record_filter_duration(
http_filter.name(),
PHASE_RESPONSE,
STREAM_BODY,
start.elapsed().as_secs_f64(),
);
result
} else {
http_filter.on_response_body(ctx, body, end_of_stream)
};
record_filter_result(&filter_span, &body_result);
dispatch_body_result(body_result, http_filter.name(), "response body", failure_mode)
}
#[expect(clippy::too_many_lines, reason = "metrics instrumentation adds branches per hook")]
pub(super) async fn run_response_filter(
http_filter: &dyn crate::filter::HttpFilter,
ctx: &mut HttpFilterContext<'_>,
failure_mode: FailureMode,
metrics_enabled: bool,
) -> Result<HeaderFilterOutcome, FilterError> {
let filter_span = debug_span!(
"filter",
"otel.name" = %format_args!("filter:{}:response", http_filter.name()),
"filter.name" = http_filter.name(),
"filter.phase" = "response",
"filter.result" = tracing::field::Empty,
);
let response_result = async {
trace!("on_response");
let result = if metrics_enabled {
let start = std::time::Instant::now();
let result = http_filter.on_response(ctx).await;
record_filter_duration(
http_filter.name(),
PHASE_RESPONSE,
STREAM_HEADERS,
start.elapsed().as_secs_f64(),
);
result
} else {
http_filter.on_response(ctx).await
};
record_filter_result(&filter_span, &result);
result
}
.instrument(filter_span.clone())
.await;
match response_result {
Ok(
FilterAction::Continue
| FilterAction::Release
| FilterAction::BodyDone
| FilterAction::TerminalResponse(_)
| FilterAction::StreamingTerminalResponse(_),
) => Ok(HeaderFilterOutcome::Continue),
Ok(FilterAction::Reject(rejection)) => {
warn!(
filter = http_filter.name(),
status = rejection.status,
"response rejected by filter"
);
Ok(HeaderFilterOutcome::Rejected(rejection))
},
Err(e) => {
check_failure_mode(http_filter.name(), e, "response", failure_mode)?;
Ok(HeaderFilterOutcome::Continue)
},
}
}
fn record_filter_result(span: &tracing::Span, result: &Result<FilterAction, FilterError>) {
let label = match result {
Ok(FilterAction::Continue) => "continue",
Ok(FilterAction::Release) => "release",
Ok(FilterAction::BodyDone) => "body_done",
Ok(FilterAction::Reject(_)) => "reject",
Ok(FilterAction::TerminalResponse(_)) => "terminal",
Ok(FilterAction::StreamingTerminalResponse(_)) => "streaming_terminal",
Err(_) => "error",
};
span.record("filter.result", label);
}
#[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 super::*;
use crate::HttpFilter;
#[test]
fn accumulate_body_bytes_some_adds_to_counter() {
let mut counter = 0_u64;
let body = Some(Bytes::from_static(b"hello"));
accumulate_body_bytes(&mut counter, body.as_ref());
assert_eq!(counter, 5, "counter should equal byte length of body");
}
#[test]
fn accumulate_body_bytes_none_does_not_change_counter() {
let mut counter = 42_u64;
accumulate_body_bytes(&mut counter, None);
assert_eq!(counter, 42, "counter should remain unchanged for None body");
}
#[test]
fn accumulate_body_bytes_multiple_sums_correctly() {
let mut counter = 0_u64;
accumulate_body_bytes(&mut counter, Some(&Bytes::from_static(b"abc")));
accumulate_body_bytes(&mut counter, Some(&Bytes::from_static(b"defgh")));
accumulate_body_bytes(&mut counter, None);
accumulate_body_bytes(&mut counter, Some(&Bytes::from_static(b"ij")));
assert_eq!(counter, 10, "counter should be sum of all Some chunk lengths");
}
#[test]
fn released_or_continue_true_returns_release() {
assert!(
matches!(released_or_continue(true), FilterAction::Release),
"true should produce FilterAction::Release"
);
}
#[test]
fn released_or_continue_false_returns_continue() {
assert!(
matches!(released_or_continue(false), FilterAction::Continue),
"false should produce FilterAction::Continue"
);
}
#[test]
fn dispatch_body_result_ok_continue() {
let outcome = dispatch_body_result(Ok(FilterAction::Continue), "test", "request", FailureMode::Closed).unwrap();
assert!(
matches!(outcome, BodyFilterOutcome::Continue),
"Ok(Continue) should produce BodyFilterOutcome::Continue"
);
}
#[test]
fn dispatch_body_result_terminal_response_is_ignored_not_honored() {
let terminal = FilterAction::TerminalResponse(Box::new(crate::TerminalResponse::new(200)));
let outcome = dispatch_body_result(Ok(terminal), "test", "request_body", FailureMode::Closed).unwrap();
assert!(
matches!(outcome, BodyFilterOutcome::Continue),
"a terminal response from a body phase is dropped and the body continues"
);
}
#[test]
fn dispatch_body_result_ok_release() {
let outcome = dispatch_body_result(Ok(FilterAction::Release), "test", "request", FailureMode::Closed).unwrap();
assert!(
matches!(outcome, BodyFilterOutcome::Released),
"Ok(Release) should produce BodyFilterOutcome::Released"
);
}
#[test]
fn dispatch_body_result_ok_reject() {
let rejection = Rejection::status(429);
let outcome = dispatch_body_result(
Ok(FilterAction::Reject(rejection)),
"test",
"request",
FailureMode::Closed,
)
.unwrap();
assert!(
matches!(&outcome, BodyFilterOutcome::Rejected(r) if r.status == 429),
"Ok(Reject(429)) should produce BodyFilterOutcome::Rejected with status 429"
);
}
#[test]
fn dispatch_body_result_ok_body_done() {
let outcome = dispatch_body_result(Ok(FilterAction::BodyDone), "test", "request", FailureMode::Closed).unwrap();
assert!(
matches!(outcome, BodyFilterOutcome::BodyDone),
"Ok(BodyDone) should produce BodyFilterOutcome::BodyDone"
);
}
#[test]
fn dispatch_body_result_err_failure_mode_open_swallows_error() {
let err: FilterError = "test error".into();
let outcome = dispatch_body_result(Err(err), "test", "request", FailureMode::Open).unwrap();
assert!(
matches!(outcome, BodyFilterOutcome::Continue),
"error with FailureMode::Open should produce BodyFilterOutcome::Continue"
);
}
#[test]
fn dispatch_body_result_err_failure_mode_closed_propagates() {
let err: FilterError = "test error".into();
let result = dispatch_body_result(Err(err), "test", "request", FailureMode::Closed);
assert!(result.is_err(), "error with FailureMode::Closed should propagate");
}
#[test]
fn skip_by_response_conditions_empty_conditions() {
let filter = crate::builtins::StaticResponseFilter::from_config(
&serde_yaml::from_str::<serde_yaml::Value>("status: 200").unwrap(),
)
.unwrap();
let req = crate::test_utils::make_request(http::Method::GET, "/");
let mut resp = crate::test_utils::make_response();
let mut ctx = crate::test_utils::make_filter_context(&req);
ctx.response_header = Some(&mut resp);
assert!(
!skip_by_response_conditions(filter.as_ref(), &[], &ctx),
"empty conditions should not skip"
);
}
#[test]
fn skip_by_response_conditions_matching_when_does_not_skip() {
use praxis_core::config::{ResponseCondition, ResponseConditionMatch};
let filter = crate::builtins::StaticResponseFilter::from_config(
&serde_yaml::from_str::<serde_yaml::Value>("status: 200").unwrap(),
)
.unwrap();
let conds = vec![ResponseCondition::When(ResponseConditionMatch {
status: Some(vec![200]),
headers: None,
})];
let req = crate::test_utils::make_request(http::Method::GET, "/");
let mut resp = crate::test_utils::make_response();
let mut ctx = crate::test_utils::make_filter_context(&req);
ctx.response_header = Some(&mut resp);
assert!(
!skip_by_response_conditions(filter.as_ref(), &conds, &ctx),
"matching 'when' condition should not skip"
);
}
#[test]
fn skip_by_response_conditions_non_matching_when_skips() {
use praxis_core::config::{ResponseCondition, ResponseConditionMatch};
let filter = crate::builtins::StaticResponseFilter::from_config(
&serde_yaml::from_str::<serde_yaml::Value>("status: 200").unwrap(),
)
.unwrap();
let conds = vec![ResponseCondition::When(ResponseConditionMatch {
status: Some(vec![404]),
headers: None,
})];
let req = crate::test_utils::make_request(http::Method::GET, "/");
let mut resp = crate::test_utils::make_response();
let mut ctx = crate::test_utils::make_filter_context(&req);
ctx.response_header = Some(&mut resp);
assert!(
skip_by_response_conditions(filter.as_ref(), &conds, &ctx),
"non-matching 'when' condition should skip"
);
}
#[test]
fn skip_by_response_conditions_no_response_header_does_not_skip() {
use praxis_core::config::{ResponseCondition, ResponseConditionMatch};
let filter = crate::builtins::StaticResponseFilter::from_config(
&serde_yaml::from_str::<serde_yaml::Value>("status: 200").unwrap(),
)
.unwrap();
let conds = vec![ResponseCondition::When(ResponseConditionMatch {
status: Some(vec![200]),
headers: None,
})];
let req = crate::test_utils::make_request(http::Method::GET, "/");
let ctx = crate::test_utils::make_filter_context(&req);
assert!(
!skip_by_response_conditions(filter.as_ref(), &conds, &ctx),
"no response header should not skip"
);
}
#[test]
fn skip_by_response_conditions_unless_match_skips() {
use http::StatusCode;
use praxis_core::config::{ResponseCondition, ResponseConditionMatch};
let req = crate::test_utils::make_request(http::Method::GET, "/");
let mut resp = crate::test_utils::make_response();
resp.status = StatusCode::BAD_REQUEST;
let mut ctx = crate::test_utils::make_filter_context(&req);
ctx.response_header = Some(&mut resp);
let conditions = vec![ResponseCondition::Unless(ResponseConditionMatch {
status: Some(vec![400]),
headers: None,
})];
let filter = StubFilter;
assert!(
skip_by_response_conditions(&filter, &conditions, &ctx),
"Unless with matching status should cause skip"
);
}
#[test]
fn dispatch_body_result_rejection_returns_status() {
let rejection = Rejection::status(413);
let outcome = dispatch_body_result(
Ok(FilterAction::Reject(rejection)),
"size_limit",
"request body",
FailureMode::Closed,
)
.unwrap();
assert!(
matches!(&outcome, BodyFilterOutcome::Rejected(r) if r.status == 413),
"body rejection should carry status 413 for span event"
);
}
#[test]
fn dispatch_body_result_response_rejection_returns_status() {
let rejection = Rejection::status(500);
let outcome = dispatch_body_result(
Ok(FilterAction::Reject(rejection)),
"transform_filter",
"response body",
FailureMode::Closed,
)
.unwrap();
assert!(
matches!(&outcome, BodyFilterOutcome::Rejected(r) if r.status == 500),
"response body rejection should carry status 500 for span event"
);
}
#[tokio::test]
async fn run_request_filter_rejection_returns_rejected_outcome() {
let filter = RejectingFilter(429);
let req = crate::test_utils::make_request(http::Method::GET, "/");
let mut ctx = crate::test_utils::make_filter_context(&req);
let outcome = run_request_filter(&filter, &mut ctx, FailureMode::Closed, false)
.await
.unwrap();
assert!(
matches!(&outcome, HeaderFilterOutcome::Rejected(r) if r.status == 429),
"rejecting filter should produce Rejected outcome with status for span event"
);
}
#[tokio::test]
async fn run_response_filter_rejection_returns_rejected_outcome() {
let filter = ResponseRejectingFilter(503);
let req = crate::test_utils::make_request(http::Method::GET, "/");
let mut ctx = crate::test_utils::make_filter_context(&req);
let outcome = run_response_filter(&filter, &mut ctx, FailureMode::Closed, false)
.await
.unwrap();
assert!(
matches!(&outcome, HeaderFilterOutcome::Rejected(r) if r.status == 503),
"response rejection should produce Rejected outcome with status for span event"
);
}
struct StubFilter;
#[async_trait::async_trait]
impl HttpFilter for StubFilter {
fn name(&self) -> &'static str {
"stub"
}
async fn on_request(&self, _ctx: &mut HttpFilterContext<'_>) -> Result<FilterAction, FilterError> {
Ok(FilterAction::Continue)
}
}
struct RejectingFilter(u16);
#[async_trait::async_trait]
impl HttpFilter for RejectingFilter {
fn name(&self) -> &'static str {
"rejecting_filter"
}
async fn on_request(&self, _ctx: &mut HttpFilterContext<'_>) -> Result<FilterAction, FilterError> {
Ok(FilterAction::Reject(Rejection::status(self.0)))
}
}
struct ResponseRejectingFilter(u16);
#[async_trait::async_trait]
impl HttpFilter for ResponseRejectingFilter {
fn name(&self) -> &'static str {
"response_rejecting_filter"
}
async fn on_request(&self, _ctx: &mut HttpFilterContext<'_>) -> Result<FilterAction, FilterError> {
Ok(FilterAction::Continue)
}
async fn on_response(&self, _ctx: &mut HttpFilterContext<'_>) -> Result<FilterAction, FilterError> {
Ok(FilterAction::Reject(Rejection::status(self.0)))
}
}
}