pub(crate) mod body;
pub(crate) mod branch;
mod build;
mod build_branch;
mod checks;
mod clusters;
pub(crate) mod evaluate;
mod extension;
pub(crate) mod filter;
mod http;
mod http_utils;
mod tcp;
#[cfg(test)]
#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::panic,
clippy::field_reassign_with_default,
clippy::type_complexity,
clippy::too_many_lines,
clippy::redundant_closure_for_method_calls,
clippy::significant_drop_tightening,
clippy::doc_markdown,
reason = "tests"
)]
mod tests;
use std::sync::Arc;
pub use extension::PipelineExtension;
use praxis_core::{
config::{ABSOLUTE_MAX_BODY_BYTES, FailureMode, InsecureOptions},
health::HealthRegistry,
id::IdGenerator,
kv::KvStoreRegistry,
time::TimeSource,
};
use tracing::warn;
use self::filter::PipelineFilter;
use crate::{
FilterError,
body::{BodyCapabilities, BodyMode},
builtins::http::payload_processing::compression_config::CompressionConfig,
extensions::RequestExtensions,
};
pub struct FilterPipeline {
body_capabilities: BodyCapabilities,
compression: Option<CompressionConfig>,
pub(crate) filters: Vec<PipelineFilter>,
health_registry: Option<HealthRegistry>,
id_generator: Arc<IdGenerator>,
kv_stores: Option<KvStoreRegistry>,
pipeline_extensions: Vec<Box<dyn PipelineExtension>>,
time_source: Arc<dyn TimeSource>,
}
#[expect(
clippy::multiple_inherent_impl,
reason = "pipeline concerns are split across modules"
)]
impl FilterPipeline {
pub fn apply_body_limits(
&mut self,
max_request: Option<usize>,
max_response: Option<usize>,
allow_unbounded: bool,
) -> Result<(), FilterError> {
if let Some(ceiling) = max_request {
self.body_capabilities.request_body_mode = clamp_body_mode(
self.body_capabilities.request_body_mode,
ceiling,
self.body_capabilities.needs_request_body,
);
self.body_capabilities.needs_request_body = true;
}
if let Some(ceiling) = max_response {
self.body_capabilities.response_body_mode = clamp_body_mode(
self.body_capabilities.response_body_mode,
ceiling,
self.body_capabilities.needs_response_body,
);
self.body_capabilities.needs_response_body = true;
}
check_unbounded_stream_buffer(
"request",
&mut self.body_capabilities.request_body_mode,
allow_unbounded,
)?;
check_unbounded_stream_buffer(
"response",
&mut self.body_capabilities.response_body_mode,
allow_unbounded,
)?;
Ok(())
}
pub fn body_capabilities(&self) -> &BodyCapabilities {
&self.body_capabilities
}
pub fn needs_body_filters(&self) -> bool {
self.body_capabilities.needs_request_body || self.body_capabilities.needs_response_body
}
pub fn len(&self) -> usize {
self.filters.len()
}
pub fn is_empty(&self) -> bool {
self.filters.is_empty()
}
pub fn compression_config(&self) -> Option<&CompressionConfig> {
self.compression.as_ref()
}
pub fn set_health_registry(&mut self, registry: HealthRegistry) {
self.health_registry = Some(registry);
}
pub fn health_registry(&self) -> Option<&HealthRegistry> {
self.health_registry.as_ref()
}
pub fn id_generator(&self) -> &IdGenerator {
&self.id_generator
}
pub fn set_id_generator(&mut self, generator: Arc<IdGenerator>) {
self.id_generator = generator;
}
pub fn kv_stores(&self) -> Option<&KvStoreRegistry> {
self.kv_stores.as_ref()
}
pub fn set_kv_stores(&mut self, stores: KvStoreRegistry) {
self.kv_stores = Some(stores);
}
pub fn add_pipeline_extension(&mut self, ext: Box<dyn PipelineExtension>) {
self.pipeline_extensions.push(ext);
}
pub fn prepare_extensions(&self, extensions: &mut RequestExtensions) {
for ext in &self.pipeline_extensions {
ext.prepare(extensions);
}
}
pub fn time_source(&self) -> &dyn TimeSource {
&*self.time_source
}
pub fn set_time_source(&mut self, source: Arc<dyn TimeSource>) {
self.time_source = source;
}
pub fn apply_insecure_options(&self, options: &InsecureOptions) {
for pf in &self.filters {
if let crate::any_filter::AnyFilter::Http(f) = &pf.filter {
f.apply_insecure_options(options);
}
}
}
}
fn clamp_body_mode(mode: BodyMode, ceiling: usize, filter_declared: bool) -> BodyMode {
match mode {
BodyMode::StreamBuffer { max_bytes } => BodyMode::StreamBuffer {
max_bytes: Some(max_bytes.map_or(ceiling, |m| m.min(ceiling))),
},
BodyMode::SizeLimit { max_bytes } => BodyMode::SizeLimit {
max_bytes: max_bytes.min(ceiling),
},
BodyMode::Stream if filter_declared => BodyMode::Stream,
BodyMode::Stream => BodyMode::SizeLimit { max_bytes: ceiling },
}
}
fn check_unbounded_stream_buffer(
direction: &str,
mode: &mut BodyMode,
allow_unbounded: bool,
) -> Result<(), FilterError> {
if let BodyMode::StreamBuffer { max_bytes: max @ None } = mode {
if allow_unbounded {
warn!(
direction = direction,
ceiling = ABSOLUTE_MAX_BODY_BYTES,
"StreamBuffer body mode has no per-filter size limit; \
clamped to absolute ceiling ({} MiB)",
ABSOLUTE_MAX_BODY_BYTES / 1_048_576
);
*max = Some(ABSOLUTE_MAX_BODY_BYTES);
} else {
return Err(format!(
"StreamBuffer {direction} body mode has no size limit; \
set max_{direction}_body_bytes or set \
insecure_options.allow_unbounded_body: true to allow"
)
.into());
}
}
Ok(())
}
pub(crate) fn check_failure_mode(
filter_name: &str,
error: FilterError,
phase: &str,
failure_mode: FailureMode,
) -> Result<(), FilterError> {
match failure_mode {
FailureMode::Open => {
warn!(
filter = filter_name,
error = %error,
"filter error during {phase}, continuing (failure_mode=open)"
);
Ok(())
},
FailureMode::Closed => {
warn!(
filter = filter_name,
error = %error,
"filter error during {phase}, aborting"
);
Err(error)
},
}
}