1use 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
16const MAX_STRUCTURED_METADATA_KEYS: usize = 64;
26
27const MAX_METADATA_ENTRIES: usize = 128;
33
34#[derive(Clone, Debug)]
45pub enum TrustedHeaderMutation {
46 Remove(HeaderName),
48
49 Set(HeaderName, http::header::HeaderValue),
54
55 Add(HeaderName, String),
63}
64
65impl TrustedHeaderMutation {
66 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#[derive(Clone, Debug, Eq, PartialEq)]
80pub enum PendingHeaderResult {
81 Absent,
83 Removed,
85 Value(String),
87}
88
89pub struct HttpFilterContext<'a> {
98 pub buffered_request_body: Option<bytes::Bytes>,
105
106 pub body_done_indices: Vec<bool>,
109
110 pub branch_iterations: HashMap<Arc<str>, u32>,
113
114 pub client_addr: Option<IpAddr>,
116
117 pub cluster: Option<Arc<str>>,
119
120 pub current_filter_id: Option<usize>,
131
132 pub downstream_tls: bool,
140
141 pub metrics_route: Option<::metrics::SharedString>,
143
144 pub peer_identity: Option<TlsPeerIdentity>,
153
154 pub extensions: RequestExtensions,
165
166 pub executed_filter_indices: Vec<bool>,
170
171 pub extra_request_headers: Vec<(Cow<'static, str>, String)>,
173
174 pub request_headers_to_remove: Vec<HeaderName>,
176
177 pub request_headers_to_set: Vec<(HeaderName, http::header::HeaderValue)>,
179
180 pub filter_metadata: HashMap<String, String>,
191
192 pub pre_read_mutations: Vec<TrustedHeaderMutation>,
196
197 pub structured_metadata: HashMap<String, serde_json::Value>,
207
208 pub filter_results: HashMap<&'static str, FilterResultSet>,
215
216 pub filter_state: HashMap<usize, Box<dyn Any + Send + Sync>>,
228
229 pub health_registry: Option<&'a HealthRegistry>,
231
232 pub id_generator: &'a IdGenerator,
234
235 pub kv_stores: Option<&'a KvStoreRegistry>,
237
238 pub subrequest_client: Option<&'a praxis_core::subrequest::SubRequestClient>,
240
241 pub request: &'a Request,
243
244 pub request_body_bytes: u64,
246
247 pub request_body_mode: BodyMode,
253
254 pub request_start: Instant,
256
257 pub response_body_bytes: u64,
259
260 pub response_body_mode: BodyMode,
266
267 pub response_header: Option<&'a mut Response>,
270
271 pub response_headers_modified: bool,
274
275 pub selected_endpoint_index: Option<usize>,
280
281 pub time_source: &'a dyn TimeSource,
283
284 pub rewritten_path: Option<String>,
300
301 pub upstream: Option<Upstream>,
303}
304
305impl HttpFilterContext<'_> {
306 pub fn cluster_name(&self) -> Option<&str> {
308 self.cluster.as_deref()
309 }
310
311 pub fn upstream_addr(&self) -> Option<&str> {
313 self.upstream.as_ref().map(|u| &*u.address)
314 }
315
316 pub(crate) fn subrequest_client(&self) -> Option<&praxis_core::subrequest::SubRequestClient> {
318 self.subrequest_client
319 }
320
321 pub fn get_metadata(&self, key: &str) -> Option<&str> {
323 self.filter_metadata.get(key).map(String::as_str)
324 }
325
326 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 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 pub fn set_request_body_mode(&mut self, mode: BodyMode) {
365 merge_body_mode(&mut self.request_body_mode, mode);
366 }
367
368 pub fn set_response_body_mode(&mut self, mode: BodyMode) {
374 merge_body_mode(&mut self.response_body_mode, mode);
375 }
376
377 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 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 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 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 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 pub fn pending_header_value(&self, name: &HeaderName) -> Result<PendingHeaderResult, String> {
469 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 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 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 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
566fn 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
590fn 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
606fn 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
616fn 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#[derive(Clone, Debug)]
650pub struct Request {
651 pub headers: HeaderMap,
653
654 pub method: Method,
656
657 pub uri: Uri,
659}
660
661#[derive(Debug)]
679pub struct Response {
680 pub headers: HeaderMap,
682
683 pub status: StatusCode,
685}
686
687#[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(®istry);
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(®istry);
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(®istry);
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(®istry);
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 #[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 #[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 #[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 #[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 #[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}