Skip to main content

praxis_filter/
context.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2024 Praxis Contributors
3
4//! Transport-agnostic HTTP request/response metadata and per-request filter context.
5
6use std::{any::Any, borrow::Cow, collections::HashMap, net::IpAddr, sync::Arc, time::Instant};
7
8use http::{HeaderMap, Method, StatusCode, Uri, header::HeaderName};
9use praxis_core::{
10    connectivity::Upstream, health::HealthRegistry, id::IdGenerator, kv::KvStoreRegistry, time::TimeSource,
11};
12use praxis_tls::TlsPeerIdentity;
13
14use crate::{body::BodyMode, extensions::RequestExtensions, pipeline::body::merge_body_mode, results::FilterResultSet};
15
16// -----------------------------------------------------------------------------
17// Constants
18// -----------------------------------------------------------------------------
19
20/// Maximum number of keys per namespace in structured metadata.
21///
22/// Prevents unbounded accumulation from streaming processors that
23/// send unique keys across many response messages. Existing keys
24/// can still be overwritten past this limit.
25const MAX_STRUCTURED_METADATA_KEYS: usize = 64;
26
27/// Maximum entries allowed in the general `filter_metadata` map.
28///
29/// Individual keys and values are already size-bounded (64 / 256
30/// bytes), but without an entry count cap a filter chain could
31/// insert thousands of unique keys per request.
32const MAX_METADATA_ENTRIES: usize = 128;
33
34/// Trusted header mutation recorded during pre-read body processing.
35///
36/// Pre-read filters run *before* the request-phase pipeline. Mutations
37/// they produce cannot be applied immediately because the request
38/// headers have already been captured. Instead, they are stored as an
39/// ordered log and replayed when the pipeline runs.
40///
41/// Downstream-supplied headers are untrusted. Only mutations in this
42/// log are considered authoritative by provenance-aware filters such
43/// as `endpoint_selector`.
44#[derive(Clone, Debug)]
45pub enum TrustedHeaderMutation {
46    /// Remove the header from the request.
47    Remove(HeaderName),
48
49    /// Set (overwrite) the header to a specific value.
50    ///
51    /// Stores [`http::header::HeaderValue`] to preserve non-text bytes
52    /// faithfully.
53    Set(HeaderName, http::header::HeaderValue),
54
55    /// Add the header with a string value.
56    ///
57    /// Uses `String` rather than [`HeaderValue`] because pre-read
58    /// `extra_request_headers` are string-typed. Trusted routing
59    /// values are always text (e.g. `host:port` addresses).
60    ///
61    /// [`HeaderValue`]: http::header::HeaderValue
62    Add(HeaderName, String),
63}
64
65impl TrustedHeaderMutation {
66    /// Whether this mutation targets the given header name.
67    pub fn matches_header(&self, name: &HeaderName) -> bool {
68        match self {
69            Self::Remove(n) | Self::Set(n, _) | Self::Add(n, _) => n == name,
70        }
71    }
72}
73
74/// Tri-state result from [`HttpFilterContext::pending_header_value`].
75///
76/// Distinguishes "not mentioned" from "explicitly removed" so that
77/// callers like `endpoint_selector` know whether to fall through to
78/// pre-read provenance.
79#[derive(Clone, Debug, Eq, PartialEq)]
80pub enum PendingHeaderResult {
81    /// The header was not mentioned in any pending mutation list.
82    Absent,
83    /// The header was explicitly removed by a pending mutation.
84    Removed,
85    /// The header has a resolved pending value.
86    Value(String),
87}
88
89/// Transport mode selected by filters for the next sub-request response.
90///
91/// This is a provider-agnostic projection of a filter decision. Praxis does
92/// not inspect request JSON or expose a YAML switch for this value.
93#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
94pub enum SubRequestResponseMode {
95    /// Buffer the complete sub-request response before returning it.
96    #[default]
97    Buffered,
98
99    /// Return response headers plus a pull-based streaming body.
100    Streaming,
101}
102
103// -----------------------------------------------------------------------------
104// HttpFilterContext
105// -----------------------------------------------------------------------------
106
107/// Per-request mutable state shared across all HTTP filters.
108///
109/// Created by the protocol layer for each incoming request. Filters read
110/// and mutate it to select clusters, choose upstreams, and inject headers.
111pub struct HttpFilterContext<'a> {
112    /// Complete request body captured by protocol pre-read, when the
113    /// pipeline's effective request mode is [`BodyMode::StreamBuffer`].
114    ///
115    /// This remains available during `on_request` even when a filter's body
116    /// hook was skipped because body-derived header mutations changed its
117    /// request conditions between the pre-read and header phases.
118    pub buffered_request_body: Option<bytes::Bytes>,
119
120    /// Per-filter body-done tracking. When `true` at index `i`,
121    /// filter `i` is skipped for remaining body chunks.
122    pub body_done_indices: Vec<bool>,
123
124    /// Iteration counters for re-entrant branches.
125    /// Branch name -> current iteration count.
126    pub branch_iterations: HashMap<Arc<str>, u32>,
127
128    /// Downstream client IP address (from the TCP connection).
129    pub client_addr: Option<IpAddr>,
130
131    /// The cluster name selected by the router filter.
132    pub cluster: Option<Arc<str>>,
133
134    /// Stable invocation ID of the filter currently executing.
135    ///
136    /// Assigned at pipeline build time and unique within the
137    /// request's pinned [`FilterPipeline`]. Set by the pipeline
138    /// executor before each filter hook call and cleared after.
139    /// Filter state accessors use this as the storage key so
140    /// that multiple instances of the same filter type — including
141    /// filters in branch chains — get independent state.
142    ///
143    /// [`FilterPipeline`]: crate::FilterPipeline
144    pub current_filter_id: Option<usize>,
145
146    /// Whether the downstream connection uses TLS.
147    ///
148    /// Set by the protocol layer from the connection's SSL
149    /// digest. Used by the forwarded headers filter to derive
150    /// `X-Forwarded-Proto` from the actual connection state
151    /// rather than the request URI scheme (which is absent
152    /// in HTTP/1.1).
153    pub downstream_tls: bool,
154
155    /// Matched route path pattern for metrics (bounded; not the raw URL).
156    pub metrics_route: Option<::metrics::SharedString>,
157
158    /// Verified downstream TLS peer identity, if the connection
159    /// is mTLS and the peer presented a valid client certificate.
160    ///
161    /// `None` for plain-TLS or non-TLS connections, or when the
162    /// client did not send a certificate (e.g. `client_cert_mode:
163    /// request` with no cert).  Populated once from the SSL digest
164    /// before the first filter runs and preserved across all
165    /// subsequent `build_filter_context()` calls for the request.
166    pub peer_identity: Option<TlsPeerIdentity>,
167
168    /// Type-safe request-scoped extension container.
169    ///
170    /// Filters store and retrieve arbitrary typed values that
171    /// persist across all Pingora lifecycle phases (request,
172    /// request body, response, response body, logging). Keyed
173    /// by [`TypeId`], so only one value per concrete type. Use
174    /// private newtypes to avoid collisions between independent
175    /// filters.
176    ///
177    /// [`TypeId`]: std::any::TypeId
178    pub extensions: RequestExtensions,
179
180    /// Tracks which pipeline filter indices actually executed
181    /// during the request phase. The response phase skips
182    /// filters that did not run (e.g. due to `SkipTo`).
183    pub executed_filter_indices: Vec<bool>,
184
185    /// Extra headers to inject into the upstream request.
186    pub extra_request_headers: Vec<(Cow<'static, str>, String)>,
187
188    /// Headers to remove from the upstream request.
189    pub request_headers_to_remove: Vec<HeaderName>,
190
191    /// Headers to set (overwrite) on the upstream request.
192    pub request_headers_to_set: Vec<(HeaderName, http::header::HeaderValue)>,
193
194    /// Durable per-request metadata that persists across all
195    /// Pingora lifecycle phases (request, request-body, response,
196    /// response-body, logging). Unlike [`filter_results`] which
197    /// are cleared after branch evaluation, metadata survives
198    /// for the entire request lifetime.
199    ///
200    /// Keys use dot-prefix namespacing by convention
201    /// (e.g. `json_rpc.kind`, `classifier.label`).
202    ///
203    /// [`filter_results`]: Self::filter_results
204    pub filter_metadata: HashMap<String, String>,
205
206    /// Ordered log of trusted header mutations from pre-read body
207    /// processing. Replayed by the protocol layer after the
208    /// request-phase pipeline runs.
209    pub pre_read_mutations: Vec<TrustedHeaderMutation>,
210
211    /// Structured per-request metadata keyed by namespace.
212    ///
213    /// Unlike [`filter_metadata`] which stores flat string
214    /// key-value pairs, this stores nested JSON values per
215    /// namespace. Used by filters that need to pass structured
216    /// data (e.g. dynamic metadata from external filters) across lifecycle
217    /// phases.
218    ///
219    /// [`filter_metadata`]: Self::filter_metadata
220    pub structured_metadata: HashMap<String, serde_json::Value>,
221
222    /// Filter result map: `filter_name` -> result entries.
223    ///
224    /// Filters write string key-value pairs here during
225    /// `on_request` or `on_response`. The pipeline executor
226    /// reads these to evaluate branch conditions. Cleared
227    /// after branch evaluation at each filter.
228    pub filter_results: HashMap<&'static str, FilterResultSet>,
229
230    /// Typed per-filter state that persists across all lifecycle
231    /// phases (request, request-body, response, response-body).
232    ///
233    /// Keyed by stable filter invocation ID, unique within the
234    /// request's pinned [`FilterPipeline`]. Swapped into each
235    /// `HttpFilterContext` from the protocol-layer request context
236    /// and written back after filter execution, following the same
237    /// pattern as [`filter_metadata`].
238    ///
239    /// [`FilterPipeline`]: crate::FilterPipeline
240    /// [`filter_metadata`]: Self::filter_metadata
241    pub filter_state: HashMap<usize, Box<dyn Any + Send + Sync>>,
242
243    /// Shared health registry for endpoint health lookups.
244    pub health_registry: Option<&'a HealthRegistry>,
245
246    /// Shared request ID generator.
247    pub id_generator: &'a IdGenerator,
248
249    /// Named key-value stores for runtime mappings.
250    pub kv_stores: Option<&'a KvStoreRegistry>,
251
252    /// Shared sub-request client for iterative sub-requests.
253    pub subrequest_client: Option<&'a praxis_core::subrequest::SubRequestClient>,
254
255    /// Filter-selected transport mode for the next sub-request response.
256    ///
257    /// Every newly constructed context starts in [`Buffered`] mode. A caller
258    /// that reuses context state across iterative steps must reset this field
259    /// before running the next step pipeline.
260    ///
261    /// [`Buffered`]: SubRequestResponseMode::Buffered
262    pub subrequest_response_mode: SubRequestResponseMode,
263
264    /// Transport-agnostic request headers, URI, and method.
265    pub request: &'a Request,
266
267    /// Accumulated request body bytes seen so far.
268    pub request_body_bytes: u64,
269
270    /// Per-request body delivery mode for the request direction.
271    /// Defaults to [`BodyMode::Stream`]; filters may upgrade it
272    /// via [`set_request_body_mode`].
273    ///
274    /// [`set_request_body_mode`]: Self::set_request_body_mode
275    pub request_body_mode: BodyMode,
276
277    /// When the request was received; available in all phases.
278    pub request_start: Instant,
279
280    /// Accumulated response body bytes seen so far.
281    pub response_body_bytes: u64,
282
283    /// Per-request body delivery mode for the response direction.
284    /// Defaults to [`BodyMode::Stream`]; filters may upgrade it
285    /// via [`set_response_body_mode`].
286    ///
287    /// [`set_response_body_mode`]: Self::set_response_body_mode
288    pub response_body_mode: BodyMode,
289
290    /// The upstream response headers, available during `on_response`.
291    /// `None` during the request phase.
292    pub response_header: Option<&'a mut Response>,
293
294    /// Optional hint that a filter modified the response headers during
295    /// `on_response`, used by the protocol layer to skip unnecessary work.
296    ///
297    /// Setting this is never required for correctness: the protocol layer
298    /// independently compares the response header name sequence before and
299    /// after the pipeline and rebuilds when it changed. Leaving it unset
300    /// only forgoes an optimisation, never an edit.
301    pub response_headers_modified: bool,
302
303    /// Index of the selected endpoint in the cluster's
304    /// endpoint list. Set by the load balancer filter
305    /// for use by passive health checking in the
306    /// protocol layer.
307    pub selected_endpoint_index: Option<usize>,
308
309    /// Wall-clock time source for timestamp generation.
310    pub time_source: &'a dyn TimeSource,
311
312    /// Rewritten URI path for the upstream request.
313    ///
314    /// Set by the `path_rewrite` or `url_rewrite` filter during
315    /// `on_request`. Applied to the upstream `RequestHeader` in the
316    /// protocol layer.
317    ///
318    /// The router checks this field before the original request URI.
319    /// If a preceding filter sets `rewritten_path`, the router
320    /// matches against it, enabling "rewrite then route" pipelines.
321    ///
322    /// If both `path_rewrite` and `url_rewrite` appear in the same
323    /// pipeline, only the last writer's value takes effect.
324    /// Pipeline validation rejects this by default; set
325    /// `allow_rewrite_override: true` on the later filter to
326    /// permit it. Or, better yet, don't.
327    pub rewritten_path: Option<String>,
328
329    /// The upstream peer selected by the load balancer filter.
330    pub upstream: Option<Upstream>,
331}
332
333impl HttpFilterContext<'_> {
334    /// Selected cluster name, if any.
335    pub fn cluster_name(&self) -> Option<&str> {
336        self.cluster.as_deref()
337    }
338
339    /// Upstream peer address, if selected.
340    pub fn upstream_addr(&self) -> Option<&str> {
341        self.upstream.as_ref().map(|u| &*u.address)
342    }
343
344    /// Shared sub-request client, if set.
345    pub(crate) fn subrequest_client(&self) -> Option<&praxis_core::subrequest::SubRequestClient> {
346        self.subrequest_client
347    }
348
349    /// Return the response transport mode selected for the next sub-request.
350    pub fn subrequest_response_mode(&self) -> SubRequestResponseMode {
351        self.subrequest_response_mode
352    }
353
354    /// Select the response transport mode for the next sub-request.
355    ///
356    /// Filters own this decision; the transport layer only executes it.
357    pub fn set_subrequest_response_mode(&mut self, mode: SubRequestResponseMode) {
358        self.subrequest_response_mode = mode;
359    }
360
361    /// Read a durable metadata value by key.
362    pub fn get_metadata(&self, key: &str) -> Option<&str> {
363        self.filter_metadata.get(key).map(String::as_str)
364    }
365
366    /// X-Request-ID header value, if present and valid UTF-8.
367    pub fn request_id(&self) -> Option<&str> {
368        self.request.headers.get("x-request-id").and_then(|v| v.to_str().ok())
369    }
370
371    /// Write a durable metadata value that persists across all phases.
372    ///
373    /// Keys should use dot-prefix namespacing
374    /// (e.g. `json_rpc.kind`, `classifier.label`). Keys are limited to
375    /// 64 bytes and values to 256 bytes to bound per-request
376    /// memory growth.
377    pub fn set_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
378        let key = key.into();
379        let value = value.into();
380        if key.is_empty() || key.len() > 64 {
381            tracing::warn!(key_len = key.len(), "metadata key rejected (must be 1-64 bytes)");
382            return;
383        }
384        if value.len() > 256 {
385            tracing::warn!(key = %key, value_len = value.len(), "metadata value rejected (max 256 bytes)");
386            return;
387        }
388        if !self.filter_metadata.contains_key(&key) && self.filter_metadata.len() >= MAX_METADATA_ENTRIES {
389            tracing::warn!(
390                key = %key,
391                entries = self.filter_metadata.len(),
392                "metadata entry rejected (max {MAX_METADATA_ENTRIES} entries)"
393            );
394            return;
395        }
396        self.filter_metadata.insert(key, value);
397    }
398
399    /// Upgrade the request body delivery mode for this request.
400    ///
401    /// Merges `mode` into the current mode using ratchet-up
402    /// semantics: `StreamBuffer > SizeLimit > Stream`. A mode
403    /// can only be upgraded, never downgraded.
404    pub fn set_request_body_mode(&mut self, mode: BodyMode) {
405        merge_body_mode(&mut self.request_body_mode, mode);
406    }
407
408    /// Upgrade the response body delivery mode for this request.
409    ///
410    /// Same ratchet-up semantics as [`set_request_body_mode`].
411    ///
412    /// [`set_request_body_mode`]: Self::set_request_body_mode
413    pub fn set_response_body_mode(&mut self, mode: BodyMode) {
414        merge_body_mode(&mut self.response_body_mode, mode);
415    }
416
417    /// Store typed per-request state for the currently executing filter.
418    ///
419    /// Uses [`current_filter_id`] as the storage key, so multiple
420    /// instances of the same filter type get independent state.
421    ///
422    /// No-op if called outside of pipeline execution (when
423    /// [`current_filter_id`] is `None`).
424    ///
425    /// [`current_filter_id`]: Self::current_filter_id
426    pub fn insert_filter_state<T: Any + Send + Sync>(&mut self, state: T) {
427        let Some(idx) = self.current_filter_id else {
428            tracing::warn!("insert_filter_state called outside pipeline execution");
429            return;
430        };
431        self.filter_state.insert(idx, Box::new(state));
432    }
433
434    /// Retrieve a shared reference to the typed state stored by the
435    /// currently executing filter.
436    ///
437    /// Returns `None` when no state is stored, when the stored type
438    /// does not match `T`, or when called outside pipeline execution.
439    pub fn get_filter_state<T: Any + Send + Sync>(&self) -> Option<&T> {
440        let idx = self.current_filter_id?;
441        self.filter_state.get(&idx)?.downcast_ref()
442    }
443
444    /// Retrieve a mutable reference to the typed state stored by the
445    /// currently executing filter.
446    ///
447    /// Returns `None` under the same conditions as
448    /// [`get_filter_state`].
449    ///
450    /// [`get_filter_state`]: Self::get_filter_state
451    pub fn get_filter_state_mut<T: Any + Send + Sync>(&mut self) -> Option<&mut T> {
452        let idx = self.current_filter_id?;
453        self.filter_state.get_mut(&idx)?.downcast_mut()
454    }
455
456    /// Remove and return the typed state stored by the currently
457    /// executing filter.
458    ///
459    /// Returns `None` when no state is stored, when the stored type
460    /// does not match `T`, or when called outside pipeline execution.
461    /// A type mismatch does not destroy the stored entry.
462    pub fn remove_filter_state<T: Any + Send + Sync>(&mut self) -> Option<T> {
463        let idx = self.current_filter_id?;
464        if !self.filter_state.get(&idx)?.as_ref().is::<T>() {
465            return None;
466        }
467        let boxed = self.filter_state.remove(&idx)?;
468        Some(*boxed.downcast::<T>().ok()?)
469    }
470
471    /// Resolve the effective value of a trusted header from the
472    /// pre-read mutation log.
473    ///
474    /// Walks the ordered mutation log forward, applying each mutation
475    /// in sequence. Only trusted sources (pre-read filter mutations)
476    /// are considered; the original request headers are intentionally
477    /// excluded.
478    ///
479    /// # Errors
480    ///
481    /// Returns an error if:
482    /// - A `Set` mutation contains a non-text [`HeaderValue`]
483    /// - Multiple distinct values remain after all mutations (ambiguous final state)
484    ///
485    /// [`HeaderValue`]: http::header::HeaderValue
486    pub fn resolve_trusted_header(&self, name: &HeaderName) -> Result<Option<String>, String> {
487        let values = collect_trusted_values(&self.pre_read_mutations, name)?;
488        require_unique_value(values, name, "trusted")
489    }
490
491    /// Resolve the effective pending value of a header from the
492    /// mutation lists (not the original request).
493    ///
494    /// Returns a tri-state [`PendingHeaderResult`] so callers can
495    /// distinguish "not mentioned" from "explicitly removed."
496    /// Applies mutations in HTTP order: remove → set → add.
497    ///
498    /// Multiple distinct values are rejected as ambiguous. This is
499    /// intentionally stricter than the normal pipeline's last-write-wins
500    /// semantics because routing-critical headers (used by
501    /// `endpoint_selector`) must have a single unambiguous value.
502    ///
503    /// # Errors
504    ///
505    /// Returns an error if a pending `Set` value contains
506    /// non-text bytes, or if the final state has multiple
507    /// distinct values.
508    pub fn pending_header_value(&self, name: &HeaderName) -> Result<PendingHeaderResult, String> {
509        // The pipeline normalizes pending mutations as remove → set → add:
510        // a remove clears any prior value, a set establishes a new one, and
511        // adds accumulate. When both remove and set are present for the same
512        // header, the set wins because it is applied after the remove.
513        let removed = self.request_headers_to_remove.iter().any(|n| n == name);
514        let set_value = find_last_set(&self.request_headers_to_set, name)?;
515        let extras = collect_extras(&self.extra_request_headers, name);
516
517        let mut all: Vec<String> = Vec::new();
518        if let Some(s) = set_value {
519            all.push(s);
520        }
521        all.extend(extras);
522
523        if all.is_empty() {
524            return Ok(if removed {
525                PendingHeaderResult::Removed
526            } else {
527                PendingHeaderResult::Absent
528            });
529        }
530
531        match require_unique_value(all, name, "pending")? {
532            Some(v) => Ok(PendingHeaderResult::Value(v)),
533            None => Ok(if removed {
534                PendingHeaderResult::Removed
535            } else {
536                PendingHeaderResult::Absent
537            }),
538        }
539    }
540
541    /// Set a structured metadata value under a namespace.
542    ///
543    /// Each namespace is stored as a JSON object; `key` becomes
544    /// a field within that object. If the namespace does not yet
545    /// exist, a new empty object is created first.
546    ///
547    /// A per-namespace key limit of 64
548    /// prevents unbounded accumulation from streaming processors.
549    /// New keys are silently dropped once the limit is reached;
550    /// existing keys can still be overwritten.
551    pub fn set_structured_metadata(&mut self, namespace: &str, key: &str, value: serde_json::Value) {
552        let ns = self
553            .structured_metadata
554            .entry(namespace.to_owned())
555            .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
556        if let serde_json::Value::Object(map) = ns {
557            if map.len() >= MAX_STRUCTURED_METADATA_KEYS && !map.contains_key(key) {
558                tracing::warn!(
559                    namespace,
560                    key,
561                    limit = MAX_STRUCTURED_METADATA_KEYS,
562                    "structured metadata key limit reached; dropping new key"
563                );
564                return;
565            }
566            map.insert(key.to_owned(), value);
567        }
568    }
569
570    /// Get a structured metadata value from a namespace.
571    ///
572    /// Returns `None` when the namespace is absent, when it is
573    /// not a JSON object, or when `key` is not present within it.
574    pub fn get_structured_metadata(&self, namespace: &str, key: &str) -> Option<&serde_json::Value> {
575        self.structured_metadata.get(namespace)?.as_object()?.get(key)
576    }
577
578    /// Merge a complete namespace object, overwriting existing keys.
579    ///
580    /// Keys already present in the namespace are overwritten;
581    /// keys absent from `values` are left untouched. New keys
582    /// that would exceed the per-namespace limit of 64
583    /// are silently dropped.
584    pub fn merge_structured_metadata(&mut self, namespace: &str, values: serde_json::Map<String, serde_json::Value>) {
585        let ns = self
586            .structured_metadata
587            .entry(namespace.to_owned())
588            .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
589        if let serde_json::Value::Object(map) = ns {
590            for (key, value) in values {
591                if map.len() >= MAX_STRUCTURED_METADATA_KEYS && !map.contains_key(&key) {
592                    tracing::warn!(
593                        namespace,
594                        key,
595                        limit = MAX_STRUCTURED_METADATA_KEYS,
596                        "structured metadata key limit reached during merge; dropping new key"
597                    );
598                    continue;
599                }
600                map.insert(key, value);
601            }
602        }
603    }
604}
605
606// -----------------------------------------------------------------------------
607// Header Resolution Helpers
608// -----------------------------------------------------------------------------
609
610/// Walk the trusted mutation log forward and collect the effective values.
611fn collect_trusted_values(mutations: &[TrustedHeaderMutation], name: &HeaderName) -> Result<Vec<String>, String> {
612    let mut values: Vec<String> = Vec::new();
613    for mutation in mutations {
614        match mutation {
615            TrustedHeaderMutation::Remove(n) if n == name => values.clear(),
616            TrustedHeaderMutation::Set(n, v) if n == name => {
617                let s = v
618                    .to_str()
619                    .map_err(|_err| format!("trusted header '{name}' contains non-text bytes"))?;
620                values.clear();
621                values.push(s.to_owned());
622            },
623            TrustedHeaderMutation::Add(n, v) if n == name => values.push(v.clone()),
624            _ => {},
625        }
626    }
627    Ok(values)
628}
629
630/// Find the last matching set value for a header name.
631fn find_last_set(
632    headers_to_set: &[(HeaderName, http::header::HeaderValue)],
633    name: &HeaderName,
634) -> Result<Option<String>, String> {
635    for (n, v) in headers_to_set.iter().rev() {
636        if n == name {
637            let s = v
638                .to_str()
639                .map_err(|_err| format!("pending header '{name}' contains non-text bytes"))?;
640            return Ok(Some(s.to_owned()));
641        }
642    }
643    Ok(None)
644}
645
646/// Collect all matching extra header values for a header name.
647fn collect_extras(extras: &[(Cow<'_, str>, String)], name: &HeaderName) -> Vec<String> {
648    let name_str = name.as_str();
649    extras
650        .iter()
651        .filter(|(n, _)| n.eq_ignore_ascii_case(name_str))
652        .map(|(_, v)| v.clone())
653        .collect()
654}
655
656/// Require that all values in a list are identical, returning the unique value.
657fn require_unique_value(values: Vec<String>, name: &HeaderName, source: &str) -> Result<Option<String>, String> {
658    let mut iter = values.into_iter();
659    let Some(first) = iter.next() else {
660        return Ok(None);
661    };
662    for v in iter {
663        if v != first {
664            return Err(format!(
665                "{source} header '{name}' has ambiguous values: '{first}' vs '{v}'"
666            ));
667        }
668    }
669    Ok(Some(first))
670}
671
672// -----------------------------------------------------------------------------
673// Request
674// -----------------------------------------------------------------------------
675
676/// HTTP request metadata.
677///
678/// ```
679/// use http::{HeaderMap, Method, Uri};
680/// use praxis_filter::Request;
681///
682/// let req = Request {
683///     method: Method::GET,
684///     uri: Uri::from_static("/api/users"),
685///     headers: HeaderMap::new(),
686/// };
687/// assert_eq!(req.uri.path(), "/api/users");
688/// ```
689#[derive(Clone, Debug)]
690pub struct Request {
691    /// HTTP header map.
692    pub headers: HeaderMap,
693
694    /// HTTP method.
695    pub method: Method,
696
697    /// Request URI.
698    pub uri: Uri,
699}
700
701// -----------------------------------------------------------------------------
702// Response
703// -----------------------------------------------------------------------------
704
705/// HTTP response metadata.
706///
707/// ```
708/// use http::{HeaderMap, StatusCode};
709/// use praxis_filter::Response;
710///
711/// let mut resp = Response {
712///     status: StatusCode::OK,
713///     headers: HeaderMap::new(),
714/// };
715/// resp.headers.insert("x-custom", "value".parse().unwrap());
716/// assert_eq!(resp.status, StatusCode::OK);
717/// ```
718#[derive(Debug)]
719pub struct Response {
720    /// HTTP header map.
721    pub headers: HeaderMap,
722
723    /// HTTP status code.
724    pub status: StatusCode,
725}
726
727// -----------------------------------------------------------------------------
728// Tests
729// -----------------------------------------------------------------------------
730
731#[cfg(test)]
732#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
733#[allow(
734    clippy::unwrap_used,
735    clippy::expect_used,
736    clippy::indexing_slicing,
737    clippy::panic,
738    reason = "tests"
739)]
740mod tests {
741    use super::*;
742
743    #[test]
744    fn request_fields_are_accessible() {
745        let req = Request {
746            method: Method::POST,
747            uri: "/submit".parse().unwrap(),
748            headers: HeaderMap::new(),
749        };
750        assert_eq!(req.method, Method::POST);
751        assert_eq!(req.uri.path(), "/submit");
752        assert!(req.headers.is_empty(), "new request should have no headers");
753    }
754
755    #[test]
756    fn response_header_mutation() {
757        let mut resp = Response {
758            status: StatusCode::OK,
759            headers: HeaderMap::new(),
760        };
761        resp.headers.insert("x-powered-by", "praxis".parse().unwrap());
762        assert_eq!(resp.headers["x-powered-by"], "praxis");
763    }
764
765    #[test]
766    fn response_status_codes() {
767        for code in [200_u16, 404, 500] {
768            let resp = Response {
769                status: StatusCode::from_u16(code).unwrap(),
770                headers: HeaderMap::new(),
771            };
772            assert_eq!(resp.status.as_u16(), code);
773        }
774    }
775
776    #[test]
777    fn cluster_name_returns_none_when_unset() {
778        let req = crate::test_utils::make_request(Method::GET, "/");
779        let ctx = crate::test_utils::make_filter_context(&req);
780        assert!(ctx.cluster_name().is_none(), "cluster name should be None when unset");
781    }
782
783    #[test]
784    fn cluster_name_returns_value_when_set() {
785        let req = crate::test_utils::make_request(Method::GET, "/");
786        let mut ctx = crate::test_utils::make_filter_context(&req);
787        ctx.cluster = Some(Arc::from("backend"));
788        assert_eq!(
789            ctx.cluster_name(),
790            Some("backend"),
791            "cluster name should return set value"
792        );
793    }
794
795    #[test]
796    fn upstream_addr_returns_none_when_unset() {
797        let req = crate::test_utils::make_request(Method::GET, "/");
798        let ctx = crate::test_utils::make_filter_context(&req);
799        assert!(ctx.upstream_addr().is_none(), "upstream addr should be None when unset");
800    }
801
802    #[test]
803    fn upstream_addr_returns_value_when_set() {
804        let req = crate::test_utils::make_request(Method::GET, "/");
805        let mut ctx = crate::test_utils::make_filter_context(&req);
806        ctx.upstream = Some(Upstream {
807            address: Arc::from("10.0.0.1:8080"),
808            tls: None,
809            connection: Arc::new(praxis_core::connectivity::ConnectionOptions::default()),
810        });
811        assert_eq!(
812            ctx.upstream_addr(),
813            Some("10.0.0.1:8080"),
814            "upstream addr should return set address"
815        );
816    }
817
818    #[test]
819    fn request_id_returns_none_when_absent() {
820        let req = crate::test_utils::make_request(Method::GET, "/");
821        let ctx = crate::test_utils::make_filter_context(&req);
822        assert!(
823            ctx.request_id().is_none(),
824            "request ID should be None when header absent"
825        );
826    }
827
828    #[test]
829    fn request_id_returns_value_when_present() {
830        let mut req = crate::test_utils::make_request(Method::GET, "/");
831        req.headers.insert("x-request-id", "abc-123".parse().unwrap());
832        let ctx = crate::test_utils::make_filter_context(&req);
833        assert_eq!(
834            ctx.request_id(),
835            Some("abc-123"),
836            "request ID should return header value"
837        );
838    }
839
840    #[test]
841    fn set_request_body_mode_upgrades_stream_to_stream_buffer() {
842        let req = crate::test_utils::make_request(Method::GET, "/");
843        let mut ctx = crate::test_utils::make_filter_context(&req);
844        assert_eq!(ctx.request_body_mode, BodyMode::Stream, "should start as Stream");
845        ctx.set_request_body_mode(BodyMode::StreamBuffer { max_bytes: Some(4096) });
846        assert_eq!(
847            ctx.request_body_mode,
848            BodyMode::StreamBuffer { max_bytes: Some(4096) },
849            "Stream should upgrade to StreamBuffer"
850        );
851    }
852
853    #[test]
854    fn set_request_body_mode_cannot_downgrade() {
855        let req = crate::test_utils::make_request(Method::GET, "/");
856        let mut ctx = crate::test_utils::make_filter_context(&req);
857        ctx.set_request_body_mode(BodyMode::StreamBuffer { max_bytes: Some(2048) });
858        ctx.set_request_body_mode(BodyMode::Stream);
859        assert_eq!(
860            ctx.request_body_mode,
861            BodyMode::StreamBuffer { max_bytes: Some(2048) },
862            "StreamBuffer should not downgrade to Stream"
863        );
864    }
865
866    #[test]
867    fn set_response_body_mode_upgrades_stream_to_stream_buffer() {
868        let req = crate::test_utils::make_request(Method::GET, "/");
869        let mut ctx = crate::test_utils::make_filter_context(&req);
870        assert_eq!(ctx.response_body_mode, BodyMode::Stream, "should start as Stream");
871        ctx.set_response_body_mode(BodyMode::StreamBuffer { max_bytes: Some(8192) });
872        assert_eq!(
873            ctx.response_body_mode,
874            BodyMode::StreamBuffer { max_bytes: Some(8192) },
875            "Stream should upgrade to StreamBuffer"
876        );
877    }
878
879    #[test]
880    fn set_request_body_mode_stream_buffer_then_stream_buffer_merges_limits() {
881        let req = crate::test_utils::make_request(Method::GET, "/");
882        let mut ctx = crate::test_utils::make_filter_context(&req);
883        ctx.set_request_body_mode(BodyMode::StreamBuffer { max_bytes: Some(2048) });
884        ctx.set_request_body_mode(BodyMode::StreamBuffer { max_bytes: Some(1024) });
885        assert_eq!(
886            ctx.request_body_mode,
887            BodyMode::StreamBuffer { max_bytes: Some(2048) },
888            "larger StreamBuffer limit should win when merging"
889        );
890    }
891
892    #[test]
893    fn get_metadata_returns_none_when_empty() {
894        let req = crate::test_utils::make_request(Method::GET, "/");
895        let ctx = crate::test_utils::make_filter_context(&req);
896        assert!(
897            ctx.get_metadata("json_rpc.method").is_none(),
898            "get_metadata should return None for absent key"
899        );
900    }
901
902    #[test]
903    fn set_metadata_then_get_returns_value() {
904        let req = crate::test_utils::make_request(Method::GET, "/");
905        let mut ctx = crate::test_utils::make_filter_context(&req);
906        ctx.set_metadata("json_rpc.method", "service/invoke");
907        assert_eq!(
908            ctx.get_metadata("json_rpc.method"),
909            Some("service/invoke"),
910            "get_metadata should return the set value"
911        );
912    }
913
914    #[test]
915    fn set_metadata_overwrites_existing() {
916        let req = crate::test_utils::make_request(Method::GET, "/");
917        let mut ctx = crate::test_utils::make_filter_context(&req);
918        ctx.set_metadata("classifier.label", "ProcessRequest");
919        ctx.set_metadata("classifier.label", "GetTask");
920        assert_eq!(
921            ctx.get_metadata("classifier.label"),
922            Some("GetTask"),
923            "set_metadata should overwrite previous value"
924        );
925    }
926
927    #[test]
928    fn metadata_independent_of_filter_results() {
929        let req = crate::test_utils::make_request(Method::GET, "/");
930        let mut ctx = crate::test_utils::make_filter_context(&req);
931        ctx.set_metadata("request.session_id", "gw-123");
932        ctx.filter_results.clear();
933        assert_eq!(
934            ctx.get_metadata("request.session_id"),
935            Some("gw-123"),
936            "clearing filter_results should not affect metadata"
937        );
938    }
939
940    #[test]
941    fn set_metadata_accepts_owned_strings() {
942        let req = crate::test_utils::make_request(Method::GET, "/");
943        let mut ctx = crate::test_utils::make_filter_context(&req);
944        let key = "request.task_id".to_owned();
945        let value = "task-456".to_owned();
946        ctx.set_metadata(key, value);
947        assert_eq!(
948            ctx.get_metadata("request.task_id"),
949            Some("task-456"),
950            "set_metadata should accept owned Strings"
951        );
952    }
953
954    #[test]
955    fn kv_stores_returns_none_when_unset() {
956        let req = crate::test_utils::make_request(Method::GET, "/");
957        let ctx = crate::test_utils::make_filter_context(&req);
958        assert!(ctx.kv_stores.is_none(), "kv_stores should be None when unset");
959    }
960
961    #[test]
962    fn kv_stores_returns_registry_when_set() {
963        let registry = KvStoreRegistry::new();
964        let store = registry.get_or_create("routing");
965        store.set("model", Arc::from("model-gamma-1"));
966
967        let req = crate::test_utils::make_request(Method::GET, "/");
968        let mut ctx = crate::test_utils::make_filter_context(&req);
969        ctx.kv_stores = Some(&registry);
970
971        let store = ctx.kv_stores.unwrap().get("routing").unwrap();
972        assert_eq!(
973            store.get("model").as_deref(),
974            Some("model-gamma-1"),
975            "filter should read KV store via context"
976        );
977    }
978
979    #[test]
980    fn kv_stores_write_from_context_is_visible() {
981        let registry = KvStoreRegistry::new();
982        let store = registry.get_or_create("flags");
983
984        let req = crate::test_utils::make_request(Method::GET, "/");
985        let mut ctx = crate::test_utils::make_filter_context(&req);
986        ctx.kv_stores = Some(&registry);
987
988        ctx.kv_stores
989            .unwrap()
990            .get("flags")
991            .unwrap()
992            .set("dark_mode", Arc::from("true"));
993        assert_eq!(
994            store.get("dark_mode").as_deref(),
995            Some("true"),
996            "write through context should be visible on the original store"
997        );
998    }
999
1000    #[test]
1001    fn kv_stores_missing_store_returns_none() {
1002        let registry = KvStoreRegistry::new();
1003
1004        let req = crate::test_utils::make_request(Method::GET, "/");
1005        let mut ctx = crate::test_utils::make_filter_context(&req);
1006        ctx.kv_stores = Some(&registry);
1007
1008        assert!(
1009            ctx.kv_stores.unwrap().get("nonexistent").is_none(),
1010            "missing store name should return None"
1011        );
1012    }
1013
1014    #[test]
1015    fn set_metadata_rejects_empty_key() {
1016        let req = crate::test_utils::make_request(Method::GET, "/");
1017        let mut ctx = crate::test_utils::make_filter_context(&req);
1018        ctx.set_metadata("", "val");
1019        assert!(ctx.get_metadata("").is_none(), "empty key should be silently rejected");
1020    }
1021
1022    #[test]
1023    fn set_metadata_rejects_long_key() {
1024        let req = crate::test_utils::make_request(Method::GET, "/");
1025        let mut ctx = crate::test_utils::make_filter_context(&req);
1026        let long_key = "k".repeat(65);
1027        ctx.set_metadata(long_key.as_str(), "val");
1028        assert!(
1029            ctx.get_metadata(long_key.as_str()).is_none(),
1030            "65-byte key should be rejected"
1031        );
1032    }
1033
1034    #[test]
1035    fn set_metadata_accepts_max_length_key() {
1036        let req = crate::test_utils::make_request(Method::GET, "/");
1037        let mut ctx = crate::test_utils::make_filter_context(&req);
1038        let max_key = "k".repeat(64);
1039        ctx.set_metadata(max_key.as_str(), "val");
1040        assert_eq!(
1041            ctx.get_metadata(max_key.as_str()),
1042            Some("val"),
1043            "64-byte key should be accepted"
1044        );
1045    }
1046
1047    #[test]
1048    fn set_metadata_rejects_long_value() {
1049        let req = crate::test_utils::make_request(Method::GET, "/");
1050        let mut ctx = crate::test_utils::make_filter_context(&req);
1051        let long_value = "v".repeat(257);
1052        ctx.set_metadata("key", long_value.as_str());
1053        assert!(ctx.get_metadata("key").is_none(), "257-byte value should be rejected");
1054    }
1055
1056    #[test]
1057    fn set_metadata_rejects_when_entry_limit_reached() {
1058        let req = crate::test_utils::make_request(Method::GET, "/");
1059        let mut ctx = crate::test_utils::make_filter_context(&req);
1060        for i in 0..MAX_METADATA_ENTRIES {
1061            ctx.set_metadata(format!("key.{i}"), "value");
1062        }
1063        assert_eq!(
1064            ctx.filter_metadata.len(),
1065            MAX_METADATA_ENTRIES,
1066            "should accept exactly {MAX_METADATA_ENTRIES} entries"
1067        );
1068
1069        ctx.set_metadata("overflow", "value");
1070        assert!(
1071            ctx.get_metadata("overflow").is_none(),
1072            "entry beyond limit should be rejected"
1073        );
1074    }
1075
1076    #[test]
1077    fn set_metadata_allows_overwrite_at_limit() {
1078        let req = crate::test_utils::make_request(Method::GET, "/");
1079        let mut ctx = crate::test_utils::make_filter_context(&req);
1080        for i in 0..MAX_METADATA_ENTRIES {
1081            ctx.set_metadata(format!("key.{i}"), "old");
1082        }
1083
1084        ctx.set_metadata("key.0", "new");
1085        assert_eq!(
1086            ctx.get_metadata("key.0"),
1087            Some("new"),
1088            "overwriting existing key at limit should succeed"
1089        );
1090        assert_eq!(
1091            ctx.filter_metadata.len(),
1092            MAX_METADATA_ENTRIES,
1093            "overwrite should not increase entry count"
1094        );
1095    }
1096
1097    #[test]
1098    fn kv_stores_lookup_with_match_types() {
1099        use praxis_core::kv::MatchType;
1100
1101        let registry = KvStoreRegistry::new();
1102        let store = registry.get_or_create("routes");
1103        store.set("route.api.v1", Arc::from("api_cluster"));
1104        store.set("route.web.main", Arc::from("web_cluster"));
1105
1106        let req = crate::test_utils::make_request(Method::GET, "/");
1107        let mut ctx = crate::test_utils::make_filter_context(&req);
1108        ctx.kv_stores = Some(&registry);
1109
1110        let store = ctx.kv_stores.unwrap().get("routes").unwrap();
1111        assert!(
1112            store.lookup("route.api", MatchType::Prefix).unwrap().is_some(),
1113            "prefix lookup should match route.api.v1"
1114        );
1115        assert!(
1116            store.lookup(".main", MatchType::Suffix).unwrap().is_some(),
1117            "suffix lookup should match route.web.main"
1118        );
1119    }
1120
1121    // -------------------------------------------------------------------------
1122    // Filter State Tests
1123    // -------------------------------------------------------------------------
1124
1125    #[test]
1126    fn insert_and_get_filter_state_returns_typed_value() {
1127        let req = crate::test_utils::make_request(Method::GET, "/");
1128        let mut ctx = crate::test_utils::make_filter_context(&req);
1129        ctx.current_filter_id = Some(0);
1130        ctx.insert_filter_state(42_u64);
1131        assert_eq!(
1132            ctx.get_filter_state::<u64>(),
1133            Some(&42_u64),
1134            "should return the inserted value"
1135        );
1136    }
1137
1138    #[test]
1139    fn get_filter_state_returns_none_when_empty() {
1140        let req = crate::test_utils::make_request(Method::GET, "/");
1141        let mut ctx = crate::test_utils::make_filter_context(&req);
1142        ctx.current_filter_id = Some(0);
1143        assert!(
1144            ctx.get_filter_state::<u64>().is_none(),
1145            "should return None when no state stored"
1146        );
1147    }
1148
1149    #[test]
1150    fn get_filter_state_returns_none_for_wrong_type() {
1151        let req = crate::test_utils::make_request(Method::GET, "/");
1152        let mut ctx = crate::test_utils::make_filter_context(&req);
1153        ctx.current_filter_id = Some(0);
1154        ctx.insert_filter_state(42_u64);
1155        assert!(
1156            ctx.get_filter_state::<String>().is_none(),
1157            "should return None for type mismatch"
1158        );
1159    }
1160
1161    #[test]
1162    fn get_filter_state_returns_none_when_no_index() {
1163        let req = crate::test_utils::make_request(Method::GET, "/");
1164        let mut ctx = crate::test_utils::make_filter_context(&req);
1165        ctx.filter_state.insert(0, Box::new(42_u64));
1166        assert!(
1167            ctx.get_filter_state::<u64>().is_none(),
1168            "should return None when current_filter_id is None"
1169        );
1170    }
1171
1172    #[test]
1173    fn get_filter_state_mut_allows_mutation() {
1174        let req = crate::test_utils::make_request(Method::GET, "/");
1175        let mut ctx = crate::test_utils::make_filter_context(&req);
1176        ctx.current_filter_id = Some(0);
1177        ctx.insert_filter_state(10_u64);
1178        *ctx.get_filter_state_mut::<u64>().unwrap() += 5;
1179        assert_eq!(
1180            ctx.get_filter_state::<u64>(),
1181            Some(&15_u64),
1182            "mutation through get_mut should be visible"
1183        );
1184    }
1185
1186    #[test]
1187    fn remove_filter_state_takes_ownership() {
1188        let req = crate::test_utils::make_request(Method::GET, "/");
1189        let mut ctx = crate::test_utils::make_filter_context(&req);
1190        ctx.current_filter_id = Some(0);
1191        ctx.insert_filter_state("hello".to_owned());
1192        let removed = ctx.remove_filter_state::<String>();
1193        assert_eq!(removed.as_deref(), Some("hello"), "should return the stored value");
1194        assert!(
1195            ctx.get_filter_state::<String>().is_none(),
1196            "state should be gone after remove"
1197        );
1198    }
1199
1200    #[test]
1201    fn remove_filter_state_returns_none_for_wrong_type() {
1202        let req = crate::test_utils::make_request(Method::GET, "/");
1203        let mut ctx = crate::test_utils::make_filter_context(&req);
1204        ctx.current_filter_id = Some(0);
1205        ctx.insert_filter_state(42_u64);
1206        assert!(
1207            ctx.remove_filter_state::<String>().is_none(),
1208            "type mismatch should return None"
1209        );
1210        assert!(
1211            ctx.get_filter_state::<u64>().is_some(),
1212            "type mismatch remove should not destroy the entry"
1213        );
1214    }
1215
1216    #[test]
1217    fn different_indices_do_not_collide() {
1218        let req = crate::test_utils::make_request(Method::GET, "/");
1219        let mut ctx = crate::test_utils::make_filter_context(&req);
1220        ctx.current_filter_id = Some(0);
1221        ctx.insert_filter_state(100_u64);
1222        ctx.current_filter_id = Some(1);
1223        ctx.insert_filter_state(200_u64);
1224
1225        ctx.current_filter_id = Some(0);
1226        assert_eq!(ctx.get_filter_state::<u64>(), Some(&100_u64), "index 0 state");
1227
1228        ctx.current_filter_id = Some(1);
1229        assert_eq!(ctx.get_filter_state::<u64>(), Some(&200_u64), "index 1 state");
1230    }
1231
1232    #[test]
1233    fn insert_filter_state_is_noop_without_index() {
1234        let req = crate::test_utils::make_request(Method::GET, "/");
1235        let mut ctx = crate::test_utils::make_filter_context(&req);
1236        ctx.insert_filter_state(42_u64);
1237        assert!(ctx.filter_state.is_empty(), "state map should remain empty");
1238    }
1239
1240    // -------------------------------------------------------------------------
1241    // TrustedHeaderMutation Tests
1242    // -------------------------------------------------------------------------
1243
1244    #[test]
1245    fn matches_header_remove() {
1246        let mutation = TrustedHeaderMutation::Remove("x-dest".parse().unwrap());
1247        assert!(mutation.matches_header(&"x-dest".parse().unwrap()));
1248        assert!(!mutation.matches_header(&"x-other".parse().unwrap()));
1249    }
1250
1251    #[test]
1252    fn matches_header_set() {
1253        let mutation = TrustedHeaderMutation::Set("x-dest".parse().unwrap(), "val".parse().unwrap());
1254        assert!(mutation.matches_header(&"x-dest".parse().unwrap()));
1255        assert!(!mutation.matches_header(&"x-other".parse().unwrap()));
1256    }
1257
1258    #[test]
1259    fn matches_header_add() {
1260        let mutation = TrustedHeaderMutation::Add("x-dest".parse().unwrap(), "val".to_owned());
1261        assert!(mutation.matches_header(&"x-dest".parse().unwrap()));
1262        assert!(!mutation.matches_header(&"x-other".parse().unwrap()));
1263    }
1264
1265    // -------------------------------------------------------------------------
1266    // resolve_trusted_header Tests
1267    // -------------------------------------------------------------------------
1268
1269    #[test]
1270    fn resolve_trusted_header_empty_log() {
1271        let req = crate::test_utils::make_request(Method::GET, "/");
1272        let ctx = crate::test_utils::make_filter_context(&req);
1273        assert_eq!(
1274            ctx.resolve_trusted_header(&"x-dest".parse().unwrap()).unwrap(),
1275            None,
1276            "empty mutation log should resolve to None"
1277        );
1278    }
1279
1280    #[test]
1281    fn resolve_trusted_header_add() {
1282        let req = crate::test_utils::make_request(Method::GET, "/");
1283        let mut ctx = crate::test_utils::make_filter_context(&req);
1284        ctx.pre_read_mutations.push(TrustedHeaderMutation::Add(
1285            "x-dest".parse().unwrap(),
1286            "host:8080".to_owned(),
1287        ));
1288        assert_eq!(
1289            ctx.resolve_trusted_header(&"x-dest".parse().unwrap()).unwrap(),
1290            Some("host:8080".to_owned()),
1291        );
1292    }
1293
1294    #[test]
1295    fn resolve_trusted_header_set() {
1296        let req = crate::test_utils::make_request(Method::GET, "/");
1297        let mut ctx = crate::test_utils::make_filter_context(&req);
1298        ctx.pre_read_mutations.push(TrustedHeaderMutation::Set(
1299            "x-dest".parse().unwrap(),
1300            "host:9090".parse().unwrap(),
1301        ));
1302        assert_eq!(
1303            ctx.resolve_trusted_header(&"x-dest".parse().unwrap()).unwrap(),
1304            Some("host:9090".to_owned()),
1305        );
1306    }
1307
1308    #[test]
1309    fn resolve_trusted_header_remove_hides_earlier_add() {
1310        let req = crate::test_utils::make_request(Method::GET, "/");
1311        let mut ctx = crate::test_utils::make_filter_context(&req);
1312        ctx.pre_read_mutations.push(TrustedHeaderMutation::Add(
1313            "x-dest".parse().unwrap(),
1314            "host:8080".to_owned(),
1315        ));
1316        ctx.pre_read_mutations
1317            .push(TrustedHeaderMutation::Remove("x-dest".parse().unwrap()));
1318        assert_eq!(
1319            ctx.resolve_trusted_header(&"x-dest".parse().unwrap()).unwrap(),
1320            None,
1321            "remove after add should resolve to None"
1322        );
1323    }
1324
1325    #[test]
1326    fn resolve_trusted_header_set_overrides_add() {
1327        let req = crate::test_utils::make_request(Method::GET, "/");
1328        let mut ctx = crate::test_utils::make_filter_context(&req);
1329        ctx.pre_read_mutations.push(TrustedHeaderMutation::Add(
1330            "x-dest".parse().unwrap(),
1331            "first:8080".to_owned(),
1332        ));
1333        ctx.pre_read_mutations.push(TrustedHeaderMutation::Set(
1334            "x-dest".parse().unwrap(),
1335            "second:9090".parse().unwrap(),
1336        ));
1337        assert_eq!(
1338            ctx.resolve_trusted_header(&"x-dest".parse().unwrap()).unwrap(),
1339            Some("second:9090".to_owned()),
1340            "set after add should override"
1341        );
1342    }
1343
1344    #[test]
1345    fn resolve_trusted_header_duplicate_add_same_value_ok() {
1346        let req = crate::test_utils::make_request(Method::GET, "/");
1347        let mut ctx = crate::test_utils::make_filter_context(&req);
1348        ctx.pre_read_mutations.push(TrustedHeaderMutation::Add(
1349            "x-dest".parse().unwrap(),
1350            "host:8080".to_owned(),
1351        ));
1352        ctx.pre_read_mutations.push(TrustedHeaderMutation::Add(
1353            "x-dest".parse().unwrap(),
1354            "host:8080".to_owned(),
1355        ));
1356        assert_eq!(
1357            ctx.resolve_trusted_header(&"x-dest".parse().unwrap()).unwrap(),
1358            Some("host:8080".to_owned()),
1359            "duplicate identical adds should be allowed"
1360        );
1361    }
1362
1363    #[test]
1364    fn resolve_trusted_header_ambiguous_add_errors() {
1365        let req = crate::test_utils::make_request(Method::GET, "/");
1366        let mut ctx = crate::test_utils::make_filter_context(&req);
1367        ctx.pre_read_mutations.push(TrustedHeaderMutation::Add(
1368            "x-dest".parse().unwrap(),
1369            "host-a:8080".to_owned(),
1370        ));
1371        ctx.pre_read_mutations.push(TrustedHeaderMutation::Add(
1372            "x-dest".parse().unwrap(),
1373            "host-b:9090".to_owned(),
1374        ));
1375        let err = ctx.resolve_trusted_header(&"x-dest".parse().unwrap()).unwrap_err();
1376        assert!(
1377            err.contains("ambiguous"),
1378            "distinct adds should produce ambiguity error: {err}"
1379        );
1380    }
1381
1382    #[test]
1383    fn resolve_trusted_header_set_then_add_same_value_ok() {
1384        let req = crate::test_utils::make_request(Method::GET, "/");
1385        let mut ctx = crate::test_utils::make_filter_context(&req);
1386        ctx.pre_read_mutations.push(TrustedHeaderMutation::Set(
1387            "x-dest".parse().unwrap(),
1388            "host:8080".parse().unwrap(),
1389        ));
1390        ctx.pre_read_mutations.push(TrustedHeaderMutation::Add(
1391            "x-dest".parse().unwrap(),
1392            "host:8080".to_owned(),
1393        ));
1394        assert_eq!(
1395            ctx.resolve_trusted_header(&"x-dest".parse().unwrap()).unwrap(),
1396            Some("host:8080".to_owned()),
1397            "set then identical add should succeed"
1398        );
1399    }
1400
1401    #[test]
1402    fn resolve_trusted_header_set_then_distinct_add_errors() {
1403        let req = crate::test_utils::make_request(Method::GET, "/");
1404        let mut ctx = crate::test_utils::make_filter_context(&req);
1405        ctx.pre_read_mutations.push(TrustedHeaderMutation::Set(
1406            "x-dest".parse().unwrap(),
1407            "host-a:8080".parse().unwrap(),
1408        ));
1409        ctx.pre_read_mutations.push(TrustedHeaderMutation::Add(
1410            "x-dest".parse().unwrap(),
1411            "host-b:9090".to_owned(),
1412        ));
1413        let err = ctx.resolve_trusted_header(&"x-dest".parse().unwrap()).unwrap_err();
1414        assert!(
1415            err.contains("ambiguous"),
1416            "set then distinct add should produce ambiguity error: {err}"
1417        );
1418    }
1419
1420    #[test]
1421    fn resolve_trusted_header_temporary_ambiguity_resolved_by_remove() {
1422        let req = crate::test_utils::make_request(Method::GET, "/");
1423        let mut ctx = crate::test_utils::make_filter_context(&req);
1424        ctx.pre_read_mutations.push(TrustedHeaderMutation::Add(
1425            "x-dest".parse().unwrap(),
1426            "host-a:8080".to_owned(),
1427        ));
1428        ctx.pre_read_mutations.push(TrustedHeaderMutation::Add(
1429            "x-dest".parse().unwrap(),
1430            "host-b:9090".to_owned(),
1431        ));
1432        ctx.pre_read_mutations
1433            .push(TrustedHeaderMutation::Remove("x-dest".parse().unwrap()));
1434        assert_eq!(
1435            ctx.resolve_trusted_header(&"x-dest".parse().unwrap()).unwrap(),
1436            None,
1437            "Add(a) -> Add(b) -> Remove should resolve to None"
1438        );
1439    }
1440
1441    #[test]
1442    fn resolve_trusted_header_temporary_ambiguity_resolved_by_set() {
1443        let req = crate::test_utils::make_request(Method::GET, "/");
1444        let mut ctx = crate::test_utils::make_filter_context(&req);
1445        ctx.pre_read_mutations.push(TrustedHeaderMutation::Add(
1446            "x-dest".parse().unwrap(),
1447            "host-a:8080".to_owned(),
1448        ));
1449        ctx.pre_read_mutations.push(TrustedHeaderMutation::Add(
1450            "x-dest".parse().unwrap(),
1451            "host-b:9090".to_owned(),
1452        ));
1453        ctx.pre_read_mutations.push(TrustedHeaderMutation::Set(
1454            "x-dest".parse().unwrap(),
1455            "final:7070".parse().unwrap(),
1456        ));
1457        assert_eq!(
1458            ctx.resolve_trusted_header(&"x-dest".parse().unwrap()).unwrap(),
1459            Some("final:7070".to_owned()),
1460            "Add(a) -> Add(b) -> Set(c) should resolve to c"
1461        );
1462    }
1463
1464    #[test]
1465    fn resolve_trusted_header_remove_then_set_produces_set() {
1466        let req = crate::test_utils::make_request(Method::GET, "/");
1467        let mut ctx = crate::test_utils::make_filter_context(&req);
1468        ctx.pre_read_mutations.push(TrustedHeaderMutation::Add(
1469            "x-dest".parse().unwrap(),
1470            "old:8080".to_owned(),
1471        ));
1472        ctx.pre_read_mutations
1473            .push(TrustedHeaderMutation::Remove("x-dest".parse().unwrap()));
1474        ctx.pre_read_mutations.push(TrustedHeaderMutation::Set(
1475            "x-dest".parse().unwrap(),
1476            "new:9090".parse().unwrap(),
1477        ));
1478        assert_eq!(
1479            ctx.resolve_trusted_header(&"x-dest".parse().unwrap()).unwrap(),
1480            Some("new:9090".to_owned()),
1481            "remove then set should produce the set value"
1482        );
1483    }
1484
1485    // -------------------------------------------------------------------------
1486    // pending_header_value Tests
1487    // -------------------------------------------------------------------------
1488
1489    #[test]
1490    fn pending_header_value_empty() {
1491        let req = crate::test_utils::make_request(Method::GET, "/");
1492        let ctx = crate::test_utils::make_filter_context(&req);
1493        assert_eq!(
1494            ctx.pending_header_value(&"x-dest".parse().unwrap()).unwrap(),
1495            PendingHeaderResult::Absent,
1496            "no pending mutations should resolve to Absent"
1497        );
1498    }
1499
1500    #[test]
1501    fn pending_header_value_from_set() {
1502        let req = crate::test_utils::make_request(Method::GET, "/");
1503        let mut ctx = crate::test_utils::make_filter_context(&req);
1504        ctx.request_headers_to_set
1505            .push(("x-dest".parse().unwrap(), "set-val:9090".parse().unwrap()));
1506        assert_eq!(
1507            ctx.pending_header_value(&"x-dest".parse().unwrap()).unwrap(),
1508            PendingHeaderResult::Value("set-val:9090".to_owned()),
1509        );
1510    }
1511
1512    #[test]
1513    fn pending_header_value_from_extra() {
1514        let req = crate::test_utils::make_request(Method::GET, "/");
1515        let mut ctx = crate::test_utils::make_filter_context(&req);
1516        ctx.extra_request_headers
1517            .push((Cow::Borrowed("x-dest"), "extra-val:7070".to_owned()));
1518        assert_eq!(
1519            ctx.pending_header_value(&"x-dest".parse().unwrap()).unwrap(),
1520            PendingHeaderResult::Value("extra-val:7070".to_owned()),
1521        );
1522    }
1523
1524    #[test]
1525    fn pending_header_value_set_after_remove_produces_set_value() {
1526        let req = crate::test_utils::make_request(Method::GET, "/");
1527        let mut ctx = crate::test_utils::make_filter_context(&req);
1528        ctx.request_headers_to_remove.push("x-dest".parse().unwrap());
1529        ctx.request_headers_to_set
1530            .push(("x-dest".parse().unwrap(), "set-val:9090".parse().unwrap()));
1531        assert_eq!(
1532            ctx.pending_header_value(&"x-dest".parse().unwrap()).unwrap(),
1533            PendingHeaderResult::Value("set-val:9090".to_owned()),
1534            "set after remove should produce the set value"
1535        );
1536    }
1537
1538    #[test]
1539    fn pending_header_value_remove_without_set_is_removed() {
1540        let req = crate::test_utils::make_request(Method::GET, "/");
1541        let mut ctx = crate::test_utils::make_filter_context(&req);
1542        ctx.request_headers_to_remove.push("x-dest".parse().unwrap());
1543        assert_eq!(
1544            ctx.pending_header_value(&"x-dest".parse().unwrap()).unwrap(),
1545            PendingHeaderResult::Removed,
1546            "remove without subsequent set should resolve to Removed"
1547        );
1548    }
1549
1550    #[test]
1551    fn pending_header_value_distinct_extras_error() {
1552        let req = crate::test_utils::make_request(Method::GET, "/");
1553        let mut ctx = crate::test_utils::make_filter_context(&req);
1554        ctx.extra_request_headers
1555            .push((Cow::Borrowed("x-dest"), "val-a:7070".to_owned()));
1556        ctx.extra_request_headers
1557            .push((Cow::Borrowed("x-dest"), "val-b:8080".to_owned()));
1558        let err = ctx.pending_header_value(&"x-dest".parse().unwrap()).unwrap_err();
1559        assert!(err.contains("ambiguous"), "distinct extras should error: {err}");
1560    }
1561
1562    // -------------------------------------------------------------------------
1563    // Structured Metadata Tests
1564    // -------------------------------------------------------------------------
1565
1566    #[test]
1567    fn structured_metadata_absent_by_default() {
1568        let req = crate::test_utils::make_request(Method::GET, "/");
1569        let ctx = crate::test_utils::make_filter_context(&req);
1570        assert!(
1571            ctx.get_structured_metadata("ns", "key").is_none(),
1572            "structured_metadata should be empty by default"
1573        );
1574    }
1575
1576    #[test]
1577    fn set_and_get_structured_metadata() {
1578        let req = crate::test_utils::make_request(Method::GET, "/");
1579        let mut ctx = crate::test_utils::make_filter_context(&req);
1580        ctx.set_structured_metadata("test_filter", "model", serde_json::json!("gpt-4"));
1581        assert_eq!(
1582            ctx.get_structured_metadata("test_filter", "model"),
1583            Some(&serde_json::json!("gpt-4")),
1584            "get should return the value set by set_structured_metadata"
1585        );
1586    }
1587
1588    #[test]
1589    fn merge_structured_metadata_overwrites_existing() {
1590        let req = crate::test_utils::make_request(Method::GET, "/");
1591        let mut ctx = crate::test_utils::make_filter_context(&req);
1592        ctx.set_structured_metadata("ns", "key", serde_json::json!("old"));
1593        let mut merge = serde_json::Map::new();
1594        merge.insert("key".to_owned(), serde_json::json!("new"));
1595        merge.insert("extra".to_owned(), serde_json::json!(42));
1596        ctx.merge_structured_metadata("ns", merge);
1597        assert_eq!(
1598            ctx.get_structured_metadata("ns", "key"),
1599            Some(&serde_json::json!("new")),
1600            "merge should overwrite existing key"
1601        );
1602        assert_eq!(
1603            ctx.get_structured_metadata("ns", "extra"),
1604            Some(&serde_json::json!(42)),
1605            "merge should add new key"
1606        );
1607    }
1608
1609    #[test]
1610    fn structured_metadata_key_limit_enforced() {
1611        let req = crate::test_utils::make_request(Method::GET, "/");
1612        let mut ctx = crate::test_utils::make_filter_context(&req);
1613        for i in 0..MAX_STRUCTURED_METADATA_KEYS {
1614            ctx.set_structured_metadata("ns", &format!("key-{i}"), serde_json::json!(i));
1615        }
1616        assert_eq!(
1617            ctx.get_structured_metadata("ns", "key-0"),
1618            Some(&serde_json::json!(0)),
1619            "first key should exist"
1620        );
1621
1622        ctx.set_structured_metadata("ns", "overflow", serde_json::json!("dropped"));
1623        assert!(
1624            ctx.get_structured_metadata("ns", "overflow").is_none(),
1625            "key beyond limit should be dropped"
1626        );
1627
1628        ctx.set_structured_metadata("ns", "key-0", serde_json::json!("updated"));
1629        assert_eq!(
1630            ctx.get_structured_metadata("ns", "key-0"),
1631            Some(&serde_json::json!("updated")),
1632            "existing key can still be overwritten past limit"
1633        );
1634    }
1635
1636    #[test]
1637    fn merge_structured_metadata_respects_key_limit() {
1638        let req = crate::test_utils::make_request(Method::GET, "/");
1639        let mut ctx = crate::test_utils::make_filter_context(&req);
1640        for i in 0..MAX_STRUCTURED_METADATA_KEYS {
1641            ctx.set_structured_metadata("ns", &format!("key-{i}"), serde_json::json!(i));
1642        }
1643
1644        let mut merge = serde_json::Map::new();
1645        merge.insert("key-0".to_owned(), serde_json::json!("overwritten"));
1646        merge.insert("new-key".to_owned(), serde_json::json!("dropped"));
1647        ctx.merge_structured_metadata("ns", merge);
1648
1649        assert_eq!(
1650            ctx.get_structured_metadata("ns", "key-0"),
1651            Some(&serde_json::json!("overwritten")),
1652            "merge should overwrite existing key past limit"
1653        );
1654        assert!(
1655            ctx.get_structured_metadata("ns", "new-key").is_none(),
1656            "merge should drop new key past limit"
1657        );
1658    }
1659}