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