praxis_filter/pipeline/
mod.rs1pub(crate) mod body;
7pub(crate) mod branch;
8mod build;
9mod build_branch;
10mod checks;
11mod clusters;
12pub(crate) mod evaluate;
13mod extension;
14pub(crate) mod filter;
15mod http;
16mod http_utils;
17mod tcp;
18
19#[cfg(test)]
20#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
21#[allow(
22 clippy::unwrap_used,
23 clippy::expect_used,
24 clippy::indexing_slicing,
25 clippy::panic,
26 clippy::field_reassign_with_default,
27 clippy::type_complexity,
28 clippy::too_many_lines,
29 clippy::redundant_closure_for_method_calls,
30 clippy::significant_drop_tightening,
31 clippy::doc_markdown,
32 reason = "tests"
33)]
34mod tests;
35
36use std::sync::Arc;
37
38pub use extension::PipelineExtension;
39use praxis_core::{
40 config::{ABSOLUTE_MAX_BODY_BYTES, FailureMode, InsecureOptions},
41 health::HealthRegistry,
42 id::IdGenerator,
43 kv::KvStoreRegistry,
44 time::TimeSource,
45};
46use tracing::warn;
47
48use self::filter::PipelineFilter;
49use crate::{
50 FilterError,
51 body::{BodyCapabilities, BodyMode},
52 builtins::http::payload_processing::compression_config::CompressionConfig,
53 extensions::RequestExtensions,
54};
55
56pub struct FilterPipeline {
70 body_capabilities: BodyCapabilities,
72
73 compression: Option<CompressionConfig>,
75
76 pub(crate) filters: Vec<PipelineFilter>,
78
79 health_registry: Option<HealthRegistry>,
81
82 id_generator: Arc<IdGenerator>,
84
85 kv_stores: Option<KvStoreRegistry>,
87
88 pipeline_extensions: Vec<Box<dyn PipelineExtension>>,
90
91 time_source: Arc<dyn TimeSource>,
93}
94
95#[expect(
96 clippy::multiple_inherent_impl,
97 reason = "pipeline concerns are split across modules"
98)]
99impl FilterPipeline {
100 pub fn apply_body_limits(
116 &mut self,
117 max_request: Option<usize>,
118 max_response: Option<usize>,
119 allow_unbounded: bool,
120 ) -> Result<(), FilterError> {
121 if let Some(ceiling) = max_request {
122 self.body_capabilities.request_body_mode = clamp_body_mode(
123 self.body_capabilities.request_body_mode,
124 ceiling,
125 self.body_capabilities.needs_request_body,
126 );
127 self.body_capabilities.needs_request_body = true;
128 }
129
130 if let Some(ceiling) = max_response {
131 self.body_capabilities.response_body_mode = clamp_body_mode(
132 self.body_capabilities.response_body_mode,
133 ceiling,
134 self.body_capabilities.needs_response_body,
135 );
136 self.body_capabilities.needs_response_body = true;
137 }
138
139 check_unbounded_stream_buffer(
140 "request",
141 &mut self.body_capabilities.request_body_mode,
142 allow_unbounded,
143 )?;
144 check_unbounded_stream_buffer(
145 "response",
146 &mut self.body_capabilities.response_body_mode,
147 allow_unbounded,
148 )?;
149
150 Ok(())
151 }
152
153 pub fn body_capabilities(&self) -> &BodyCapabilities {
155 &self.body_capabilities
156 }
157
158 pub fn needs_body_filters(&self) -> bool {
160 self.body_capabilities.needs_request_body || self.body_capabilities.needs_response_body
161 }
162
163 pub fn len(&self) -> usize {
165 self.filters.len()
166 }
167
168 pub fn is_empty(&self) -> bool {
170 self.filters.is_empty()
171 }
172
173 pub fn compression_config(&self) -> Option<&CompressionConfig> {
175 self.compression.as_ref()
176 }
177
178 pub fn set_health_registry(&mut self, registry: HealthRegistry) {
180 self.health_registry = Some(registry);
181 }
182
183 pub fn health_registry(&self) -> Option<&HealthRegistry> {
185 self.health_registry.as_ref()
186 }
187
188 pub fn id_generator(&self) -> &IdGenerator {
190 &self.id_generator
191 }
192
193 pub fn set_id_generator(&mut self, generator: Arc<IdGenerator>) {
195 self.id_generator = generator;
196 }
197
198 pub fn kv_stores(&self) -> Option<&KvStoreRegistry> {
200 self.kv_stores.as_ref()
201 }
202
203 pub fn set_kv_stores(&mut self, stores: KvStoreRegistry) {
205 self.kv_stores = Some(stores);
206 }
207
208 pub fn add_pipeline_extension(&mut self, ext: Box<dyn PipelineExtension>) {
217 self.pipeline_extensions.push(ext);
218 }
219
220 pub fn prepare_extensions(&self, extensions: &mut RequestExtensions) {
226 for ext in &self.pipeline_extensions {
227 ext.prepare(extensions);
228 }
229 }
230
231 pub fn time_source(&self) -> &dyn TimeSource {
233 &*self.time_source
234 }
235
236 pub fn set_time_source(&mut self, source: Arc<dyn TimeSource>) {
238 self.time_source = source;
239 }
240
241 pub fn apply_insecure_options(&self, options: &InsecureOptions) {
250 for pf in &self.filters {
251 if let crate::any_filter::AnyFilter::Http(f) = &pf.filter {
252 f.apply_insecure_options(options);
253 }
254 }
255 }
256}
257
258fn clamp_body_mode(mode: BodyMode, ceiling: usize, filter_declared: bool) -> BodyMode {
268 match mode {
269 BodyMode::StreamBuffer { max_bytes } => BodyMode::StreamBuffer {
270 max_bytes: Some(max_bytes.map_or(ceiling, |m| m.min(ceiling))),
271 },
272 BodyMode::SizeLimit { max_bytes } => BodyMode::SizeLimit {
273 max_bytes: max_bytes.min(ceiling),
274 },
275 BodyMode::Stream if filter_declared => BodyMode::Stream,
276 BodyMode::Stream => BodyMode::SizeLimit { max_bytes: ceiling },
277 }
278}
279
280fn check_unbounded_stream_buffer(
293 direction: &str,
294 mode: &mut BodyMode,
295 allow_unbounded: bool,
296) -> Result<(), FilterError> {
297 if let BodyMode::StreamBuffer { max_bytes: max @ None } = mode {
298 if allow_unbounded {
299 warn!(
300 direction = direction,
301 ceiling = ABSOLUTE_MAX_BODY_BYTES,
302 "StreamBuffer body mode has no per-filter size limit; \
303 clamped to absolute ceiling ({} MiB)",
304 ABSOLUTE_MAX_BODY_BYTES / 1_048_576
305 );
306 *max = Some(ABSOLUTE_MAX_BODY_BYTES);
307 } else {
308 return Err(format!(
309 "StreamBuffer {direction} body mode has no size limit; \
310 set max_{direction}_body_bytes or set \
311 insecure_options.allow_unbounded_body: true to allow"
312 )
313 .into());
314 }
315 }
316 Ok(())
317}
318
319pub(crate) fn check_failure_mode(
328 filter_name: &str,
329 error: FilterError,
330 phase: &str,
331 failure_mode: FailureMode,
332) -> Result<(), FilterError> {
333 match failure_mode {
334 FailureMode::Open => {
335 warn!(
336 filter = filter_name,
337 error = %error,
338 "filter error during {phase}, continuing (failure_mode=open)"
339 );
340 Ok(())
341 },
342 FailureMode::Closed => {
343 warn!(
344 filter = filter_name,
345 error = %error,
346 "filter error during {phase}, aborting"
347 );
348 Err(error)
349 },
350 }
351}