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
89#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
94pub enum SubRequestResponseMode {
95 #[default]
97 Buffered,
98
99 Streaming,
101}
102
103pub struct HttpFilterContext<'a> {
112 pub buffered_request_body: Option<bytes::Bytes>,
119
120 pub body_done_indices: Vec<bool>,
123
124 pub branch_iterations: HashMap<Arc<str>, u32>,
127
128 pub client_addr: Option<IpAddr>,
130
131 pub cluster: Option<Arc<str>>,
133
134 pub current_filter_id: Option<usize>,
145
146 pub downstream_tls: bool,
154
155 pub metrics_route: Option<::metrics::SharedString>,
157
158 pub peer_identity: Option<TlsPeerIdentity>,
167
168 pub extensions: RequestExtensions,
179
180 pub executed_filter_indices: Vec<bool>,
184
185 pub extra_request_headers: Vec<(Cow<'static, str>, String)>,
187
188 pub request_headers_to_remove: Vec<HeaderName>,
190
191 pub request_headers_to_set: Vec<(HeaderName, http::header::HeaderValue)>,
193
194 pub filter_metadata: HashMap<String, String>,
205
206 pub pre_read_mutations: Vec<TrustedHeaderMutation>,
210
211 pub structured_metadata: HashMap<String, serde_json::Value>,
221
222 pub filter_results: HashMap<&'static str, FilterResultSet>,
229
230 pub filter_state: HashMap<usize, Box<dyn Any + Send + Sync>>,
242
243 pub health_registry: Option<&'a HealthRegistry>,
245
246 pub id_generator: &'a IdGenerator,
248
249 pub kv_stores: Option<&'a KvStoreRegistry>,
251
252 pub subrequest_client: Option<&'a praxis_core::subrequest::SubRequestClient>,
254
255 pub subrequest_response_mode: SubRequestResponseMode,
263
264 pub request: &'a Request,
266
267 pub request_body_bytes: u64,
269
270 pub request_body_mode: BodyMode,
276
277 pub request_start: Instant,
279
280 pub response_body_bytes: u64,
282
283 pub response_body_mode: BodyMode,
289
290 pub response_header: Option<&'a mut Response>,
293
294 pub response_headers_modified: bool,
302
303 pub selected_endpoint_index: Option<usize>,
308
309 pub time_source: &'a dyn TimeSource,
311
312 pub rewritten_path: Option<String>,
328
329 pub upstream: Option<Upstream>,
331}
332
333impl HttpFilterContext<'_> {
334 pub fn cluster_name(&self) -> Option<&str> {
336 self.cluster.as_deref()
337 }
338
339 pub fn upstream_addr(&self) -> Option<&str> {
341 self.upstream.as_ref().map(|u| &*u.address)
342 }
343
344 pub(crate) fn subrequest_client(&self) -> Option<&praxis_core::subrequest::SubRequestClient> {
346 self.subrequest_client
347 }
348
349 pub fn subrequest_response_mode(&self) -> SubRequestResponseMode {
351 self.subrequest_response_mode
352 }
353
354 pub fn set_subrequest_response_mode(&mut self, mode: SubRequestResponseMode) {
358 self.subrequest_response_mode = mode;
359 }
360
361 pub fn get_metadata(&self, key: &str) -> Option<&str> {
363 self.filter_metadata.get(key).map(String::as_str)
364 }
365
366 pub fn request_id(&self) -> Option<&str> {
368 self.request.headers.get("x-request-id").and_then(|v| v.to_str().ok())
369 }
370
371 pub fn set_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
378 let key = key.into();
379 let value = value.into();
380 if key.is_empty() || key.len() > 64 {
381 tracing::warn!(key_len = key.len(), "metadata key rejected (must be 1-64 bytes)");
382 return;
383 }
384 if value.len() > 256 {
385 tracing::warn!(key = %key, value_len = value.len(), "metadata value rejected (max 256 bytes)");
386 return;
387 }
388 if !self.filter_metadata.contains_key(&key) && self.filter_metadata.len() >= MAX_METADATA_ENTRIES {
389 tracing::warn!(
390 key = %key,
391 entries = self.filter_metadata.len(),
392 "metadata entry rejected (max {MAX_METADATA_ENTRIES} entries)"
393 );
394 return;
395 }
396 self.filter_metadata.insert(key, value);
397 }
398
399 pub fn set_request_body_mode(&mut self, mode: BodyMode) {
405 merge_body_mode(&mut self.request_body_mode, mode);
406 }
407
408 pub fn set_response_body_mode(&mut self, mode: BodyMode) {
414 merge_body_mode(&mut self.response_body_mode, mode);
415 }
416
417 pub fn insert_filter_state<T: Any + Send + Sync>(&mut self, state: T) {
427 let Some(idx) = self.current_filter_id else {
428 tracing::warn!("insert_filter_state called outside pipeline execution");
429 return;
430 };
431 self.filter_state.insert(idx, Box::new(state));
432 }
433
434 pub fn get_filter_state<T: Any + Send + Sync>(&self) -> Option<&T> {
440 let idx = self.current_filter_id?;
441 self.filter_state.get(&idx)?.downcast_ref()
442 }
443
444 pub fn get_filter_state_mut<T: Any + Send + Sync>(&mut self) -> Option<&mut T> {
452 let idx = self.current_filter_id?;
453 self.filter_state.get_mut(&idx)?.downcast_mut()
454 }
455
456 pub fn remove_filter_state<T: Any + Send + Sync>(&mut self) -> Option<T> {
463 let idx = self.current_filter_id?;
464 if !self.filter_state.get(&idx)?.as_ref().is::<T>() {
465 return None;
466 }
467 let boxed = self.filter_state.remove(&idx)?;
468 Some(*boxed.downcast::<T>().ok()?)
469 }
470
471 pub fn resolve_trusted_header(&self, name: &HeaderName) -> Result<Option<String>, String> {
487 let values = collect_trusted_values(&self.pre_read_mutations, name)?;
488 require_unique_value(values, name, "trusted")
489 }
490
491 pub fn pending_header_value(&self, name: &HeaderName) -> Result<PendingHeaderResult, String> {
509 let removed = self.request_headers_to_remove.iter().any(|n| n == name);
514 let set_value = find_last_set(&self.request_headers_to_set, name)?;
515 let extras = collect_extras(&self.extra_request_headers, name);
516
517 let mut all: Vec<String> = Vec::new();
518 if let Some(s) = set_value {
519 all.push(s);
520 }
521 all.extend(extras);
522
523 if all.is_empty() {
524 return Ok(if removed {
525 PendingHeaderResult::Removed
526 } else {
527 PendingHeaderResult::Absent
528 });
529 }
530
531 match require_unique_value(all, name, "pending")? {
532 Some(v) => Ok(PendingHeaderResult::Value(v)),
533 None => Ok(if removed {
534 PendingHeaderResult::Removed
535 } else {
536 PendingHeaderResult::Absent
537 }),
538 }
539 }
540
541 pub fn set_structured_metadata(&mut self, namespace: &str, key: &str, value: serde_json::Value) {
552 let ns = self
553 .structured_metadata
554 .entry(namespace.to_owned())
555 .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
556 if let serde_json::Value::Object(map) = ns {
557 if map.len() >= MAX_STRUCTURED_METADATA_KEYS && !map.contains_key(key) {
558 tracing::warn!(
559 namespace,
560 key,
561 limit = MAX_STRUCTURED_METADATA_KEYS,
562 "structured metadata key limit reached; dropping new key"
563 );
564 return;
565 }
566 map.insert(key.to_owned(), value);
567 }
568 }
569
570 pub fn get_structured_metadata(&self, namespace: &str, key: &str) -> Option<&serde_json::Value> {
575 self.structured_metadata.get(namespace)?.as_object()?.get(key)
576 }
577
578 pub fn merge_structured_metadata(&mut self, namespace: &str, values: serde_json::Map<String, serde_json::Value>) {
585 let ns = self
586 .structured_metadata
587 .entry(namespace.to_owned())
588 .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
589 if let serde_json::Value::Object(map) = ns {
590 for (key, value) in values {
591 if map.len() >= MAX_STRUCTURED_METADATA_KEYS && !map.contains_key(&key) {
592 tracing::warn!(
593 namespace,
594 key,
595 limit = MAX_STRUCTURED_METADATA_KEYS,
596 "structured metadata key limit reached during merge; dropping new key"
597 );
598 continue;
599 }
600 map.insert(key, value);
601 }
602 }
603 }
604}
605
606fn collect_trusted_values(mutations: &[TrustedHeaderMutation], name: &HeaderName) -> Result<Vec<String>, String> {
612 let mut values: Vec<String> = Vec::new();
613 for mutation in mutations {
614 match mutation {
615 TrustedHeaderMutation::Remove(n) if n == name => values.clear(),
616 TrustedHeaderMutation::Set(n, v) if n == name => {
617 let s = v
618 .to_str()
619 .map_err(|_err| format!("trusted header '{name}' contains non-text bytes"))?;
620 values.clear();
621 values.push(s.to_owned());
622 },
623 TrustedHeaderMutation::Add(n, v) if n == name => values.push(v.clone()),
624 _ => {},
625 }
626 }
627 Ok(values)
628}
629
630fn find_last_set(
632 headers_to_set: &[(HeaderName, http::header::HeaderValue)],
633 name: &HeaderName,
634) -> Result<Option<String>, String> {
635 for (n, v) in headers_to_set.iter().rev() {
636 if n == name {
637 let s = v
638 .to_str()
639 .map_err(|_err| format!("pending header '{name}' contains non-text bytes"))?;
640 return Ok(Some(s.to_owned()));
641 }
642 }
643 Ok(None)
644}
645
646fn collect_extras(extras: &[(Cow<'_, str>, String)], name: &HeaderName) -> Vec<String> {
648 let name_str = name.as_str();
649 extras
650 .iter()
651 .filter(|(n, _)| n.eq_ignore_ascii_case(name_str))
652 .map(|(_, v)| v.clone())
653 .collect()
654}
655
656fn require_unique_value(values: Vec<String>, name: &HeaderName, source: &str) -> Result<Option<String>, String> {
658 let mut iter = values.into_iter();
659 let Some(first) = iter.next() else {
660 return Ok(None);
661 };
662 for v in iter {
663 if v != first {
664 return Err(format!(
665 "{source} header '{name}' has ambiguous values: '{first}' vs '{v}'"
666 ));
667 }
668 }
669 Ok(Some(first))
670}
671
672#[derive(Clone, Debug)]
690pub struct Request {
691 pub headers: HeaderMap,
693
694 pub method: Method,
696
697 pub uri: Uri,
699}
700
701#[derive(Debug)]
719pub struct Response {
720 pub headers: HeaderMap,
722
723 pub status: StatusCode,
725}
726
727#[cfg(test)]
732#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
733#[allow(
734 clippy::unwrap_used,
735 clippy::expect_used,
736 clippy::indexing_slicing,
737 clippy::panic,
738 reason = "tests"
739)]
740mod tests {
741 use super::*;
742
743 #[test]
744 fn request_fields_are_accessible() {
745 let req = Request {
746 method: Method::POST,
747 uri: "/submit".parse().unwrap(),
748 headers: HeaderMap::new(),
749 };
750 assert_eq!(req.method, Method::POST);
751 assert_eq!(req.uri.path(), "/submit");
752 assert!(req.headers.is_empty(), "new request should have no headers");
753 }
754
755 #[test]
756 fn response_header_mutation() {
757 let mut resp = Response {
758 status: StatusCode::OK,
759 headers: HeaderMap::new(),
760 };
761 resp.headers.insert("x-powered-by", "praxis".parse().unwrap());
762 assert_eq!(resp.headers["x-powered-by"], "praxis");
763 }
764
765 #[test]
766 fn response_status_codes() {
767 for code in [200_u16, 404, 500] {
768 let resp = Response {
769 status: StatusCode::from_u16(code).unwrap(),
770 headers: HeaderMap::new(),
771 };
772 assert_eq!(resp.status.as_u16(), code);
773 }
774 }
775
776 #[test]
777 fn cluster_name_returns_none_when_unset() {
778 let req = crate::test_utils::make_request(Method::GET, "/");
779 let ctx = crate::test_utils::make_filter_context(&req);
780 assert!(ctx.cluster_name().is_none(), "cluster name should be None when unset");
781 }
782
783 #[test]
784 fn cluster_name_returns_value_when_set() {
785 let req = crate::test_utils::make_request(Method::GET, "/");
786 let mut ctx = crate::test_utils::make_filter_context(&req);
787 ctx.cluster = Some(Arc::from("backend"));
788 assert_eq!(
789 ctx.cluster_name(),
790 Some("backend"),
791 "cluster name should return set value"
792 );
793 }
794
795 #[test]
796 fn upstream_addr_returns_none_when_unset() {
797 let req = crate::test_utils::make_request(Method::GET, "/");
798 let ctx = crate::test_utils::make_filter_context(&req);
799 assert!(ctx.upstream_addr().is_none(), "upstream addr should be None when unset");
800 }
801
802 #[test]
803 fn upstream_addr_returns_value_when_set() {
804 let req = crate::test_utils::make_request(Method::GET, "/");
805 let mut ctx = crate::test_utils::make_filter_context(&req);
806 ctx.upstream = Some(Upstream {
807 address: Arc::from("10.0.0.1:8080"),
808 tls: None,
809 connection: Arc::new(praxis_core::connectivity::ConnectionOptions::default()),
810 });
811 assert_eq!(
812 ctx.upstream_addr(),
813 Some("10.0.0.1:8080"),
814 "upstream addr should return set address"
815 );
816 }
817
818 #[test]
819 fn request_id_returns_none_when_absent() {
820 let req = crate::test_utils::make_request(Method::GET, "/");
821 let ctx = crate::test_utils::make_filter_context(&req);
822 assert!(
823 ctx.request_id().is_none(),
824 "request ID should be None when header absent"
825 );
826 }
827
828 #[test]
829 fn request_id_returns_value_when_present() {
830 let mut req = crate::test_utils::make_request(Method::GET, "/");
831 req.headers.insert("x-request-id", "abc-123".parse().unwrap());
832 let ctx = crate::test_utils::make_filter_context(&req);
833 assert_eq!(
834 ctx.request_id(),
835 Some("abc-123"),
836 "request ID should return header value"
837 );
838 }
839
840 #[test]
841 fn set_request_body_mode_upgrades_stream_to_stream_buffer() {
842 let req = crate::test_utils::make_request(Method::GET, "/");
843 let mut ctx = crate::test_utils::make_filter_context(&req);
844 assert_eq!(ctx.request_body_mode, BodyMode::Stream, "should start as Stream");
845 ctx.set_request_body_mode(BodyMode::StreamBuffer { max_bytes: Some(4096) });
846 assert_eq!(
847 ctx.request_body_mode,
848 BodyMode::StreamBuffer { max_bytes: Some(4096) },
849 "Stream should upgrade to StreamBuffer"
850 );
851 }
852
853 #[test]
854 fn set_request_body_mode_cannot_downgrade() {
855 let req = crate::test_utils::make_request(Method::GET, "/");
856 let mut ctx = crate::test_utils::make_filter_context(&req);
857 ctx.set_request_body_mode(BodyMode::StreamBuffer { max_bytes: Some(2048) });
858 ctx.set_request_body_mode(BodyMode::Stream);
859 assert_eq!(
860 ctx.request_body_mode,
861 BodyMode::StreamBuffer { max_bytes: Some(2048) },
862 "StreamBuffer should not downgrade to Stream"
863 );
864 }
865
866 #[test]
867 fn set_response_body_mode_upgrades_stream_to_stream_buffer() {
868 let req = crate::test_utils::make_request(Method::GET, "/");
869 let mut ctx = crate::test_utils::make_filter_context(&req);
870 assert_eq!(ctx.response_body_mode, BodyMode::Stream, "should start as Stream");
871 ctx.set_response_body_mode(BodyMode::StreamBuffer { max_bytes: Some(8192) });
872 assert_eq!(
873 ctx.response_body_mode,
874 BodyMode::StreamBuffer { max_bytes: Some(8192) },
875 "Stream should upgrade to StreamBuffer"
876 );
877 }
878
879 #[test]
880 fn set_request_body_mode_stream_buffer_then_stream_buffer_merges_limits() {
881 let req = crate::test_utils::make_request(Method::GET, "/");
882 let mut ctx = crate::test_utils::make_filter_context(&req);
883 ctx.set_request_body_mode(BodyMode::StreamBuffer { max_bytes: Some(2048) });
884 ctx.set_request_body_mode(BodyMode::StreamBuffer { max_bytes: Some(1024) });
885 assert_eq!(
886 ctx.request_body_mode,
887 BodyMode::StreamBuffer { max_bytes: Some(2048) },
888 "larger StreamBuffer limit should win when merging"
889 );
890 }
891
892 #[test]
893 fn get_metadata_returns_none_when_empty() {
894 let req = crate::test_utils::make_request(Method::GET, "/");
895 let ctx = crate::test_utils::make_filter_context(&req);
896 assert!(
897 ctx.get_metadata("json_rpc.method").is_none(),
898 "get_metadata should return None for absent key"
899 );
900 }
901
902 #[test]
903 fn set_metadata_then_get_returns_value() {
904 let req = crate::test_utils::make_request(Method::GET, "/");
905 let mut ctx = crate::test_utils::make_filter_context(&req);
906 ctx.set_metadata("json_rpc.method", "service/invoke");
907 assert_eq!(
908 ctx.get_metadata("json_rpc.method"),
909 Some("service/invoke"),
910 "get_metadata should return the set value"
911 );
912 }
913
914 #[test]
915 fn set_metadata_overwrites_existing() {
916 let req = crate::test_utils::make_request(Method::GET, "/");
917 let mut ctx = crate::test_utils::make_filter_context(&req);
918 ctx.set_metadata("classifier.label", "ProcessRequest");
919 ctx.set_metadata("classifier.label", "GetTask");
920 assert_eq!(
921 ctx.get_metadata("classifier.label"),
922 Some("GetTask"),
923 "set_metadata should overwrite previous value"
924 );
925 }
926
927 #[test]
928 fn metadata_independent_of_filter_results() {
929 let req = crate::test_utils::make_request(Method::GET, "/");
930 let mut ctx = crate::test_utils::make_filter_context(&req);
931 ctx.set_metadata("request.session_id", "gw-123");
932 ctx.filter_results.clear();
933 assert_eq!(
934 ctx.get_metadata("request.session_id"),
935 Some("gw-123"),
936 "clearing filter_results should not affect metadata"
937 );
938 }
939
940 #[test]
941 fn set_metadata_accepts_owned_strings() {
942 let req = crate::test_utils::make_request(Method::GET, "/");
943 let mut ctx = crate::test_utils::make_filter_context(&req);
944 let key = "request.task_id".to_owned();
945 let value = "task-456".to_owned();
946 ctx.set_metadata(key, value);
947 assert_eq!(
948 ctx.get_metadata("request.task_id"),
949 Some("task-456"),
950 "set_metadata should accept owned Strings"
951 );
952 }
953
954 #[test]
955 fn kv_stores_returns_none_when_unset() {
956 let req = crate::test_utils::make_request(Method::GET, "/");
957 let ctx = crate::test_utils::make_filter_context(&req);
958 assert!(ctx.kv_stores.is_none(), "kv_stores should be None when unset");
959 }
960
961 #[test]
962 fn kv_stores_returns_registry_when_set() {
963 let registry = KvStoreRegistry::new();
964 let store = registry.get_or_create("routing");
965 store.set("model", Arc::from("model-gamma-1"));
966
967 let req = crate::test_utils::make_request(Method::GET, "/");
968 let mut ctx = crate::test_utils::make_filter_context(&req);
969 ctx.kv_stores = Some(®istry);
970
971 let store = ctx.kv_stores.unwrap().get("routing").unwrap();
972 assert_eq!(
973 store.get("model").as_deref(),
974 Some("model-gamma-1"),
975 "filter should read KV store via context"
976 );
977 }
978
979 #[test]
980 fn kv_stores_write_from_context_is_visible() {
981 let registry = KvStoreRegistry::new();
982 let store = registry.get_or_create("flags");
983
984 let req = crate::test_utils::make_request(Method::GET, "/");
985 let mut ctx = crate::test_utils::make_filter_context(&req);
986 ctx.kv_stores = Some(®istry);
987
988 ctx.kv_stores
989 .unwrap()
990 .get("flags")
991 .unwrap()
992 .set("dark_mode", Arc::from("true"));
993 assert_eq!(
994 store.get("dark_mode").as_deref(),
995 Some("true"),
996 "write through context should be visible on the original store"
997 );
998 }
999
1000 #[test]
1001 fn kv_stores_missing_store_returns_none() {
1002 let registry = KvStoreRegistry::new();
1003
1004 let req = crate::test_utils::make_request(Method::GET, "/");
1005 let mut ctx = crate::test_utils::make_filter_context(&req);
1006 ctx.kv_stores = Some(®istry);
1007
1008 assert!(
1009 ctx.kv_stores.unwrap().get("nonexistent").is_none(),
1010 "missing store name should return None"
1011 );
1012 }
1013
1014 #[test]
1015 fn set_metadata_rejects_empty_key() {
1016 let req = crate::test_utils::make_request(Method::GET, "/");
1017 let mut ctx = crate::test_utils::make_filter_context(&req);
1018 ctx.set_metadata("", "val");
1019 assert!(ctx.get_metadata("").is_none(), "empty key should be silently rejected");
1020 }
1021
1022 #[test]
1023 fn set_metadata_rejects_long_key() {
1024 let req = crate::test_utils::make_request(Method::GET, "/");
1025 let mut ctx = crate::test_utils::make_filter_context(&req);
1026 let long_key = "k".repeat(65);
1027 ctx.set_metadata(long_key.as_str(), "val");
1028 assert!(
1029 ctx.get_metadata(long_key.as_str()).is_none(),
1030 "65-byte key should be rejected"
1031 );
1032 }
1033
1034 #[test]
1035 fn set_metadata_accepts_max_length_key() {
1036 let req = crate::test_utils::make_request(Method::GET, "/");
1037 let mut ctx = crate::test_utils::make_filter_context(&req);
1038 let max_key = "k".repeat(64);
1039 ctx.set_metadata(max_key.as_str(), "val");
1040 assert_eq!(
1041 ctx.get_metadata(max_key.as_str()),
1042 Some("val"),
1043 "64-byte key should be accepted"
1044 );
1045 }
1046
1047 #[test]
1048 fn set_metadata_rejects_long_value() {
1049 let req = crate::test_utils::make_request(Method::GET, "/");
1050 let mut ctx = crate::test_utils::make_filter_context(&req);
1051 let long_value = "v".repeat(257);
1052 ctx.set_metadata("key", long_value.as_str());
1053 assert!(ctx.get_metadata("key").is_none(), "257-byte value should be rejected");
1054 }
1055
1056 #[test]
1057 fn set_metadata_rejects_when_entry_limit_reached() {
1058 let req = crate::test_utils::make_request(Method::GET, "/");
1059 let mut ctx = crate::test_utils::make_filter_context(&req);
1060 for i in 0..MAX_METADATA_ENTRIES {
1061 ctx.set_metadata(format!("key.{i}"), "value");
1062 }
1063 assert_eq!(
1064 ctx.filter_metadata.len(),
1065 MAX_METADATA_ENTRIES,
1066 "should accept exactly {MAX_METADATA_ENTRIES} entries"
1067 );
1068
1069 ctx.set_metadata("overflow", "value");
1070 assert!(
1071 ctx.get_metadata("overflow").is_none(),
1072 "entry beyond limit should be rejected"
1073 );
1074 }
1075
1076 #[test]
1077 fn set_metadata_allows_overwrite_at_limit() {
1078 let req = crate::test_utils::make_request(Method::GET, "/");
1079 let mut ctx = crate::test_utils::make_filter_context(&req);
1080 for i in 0..MAX_METADATA_ENTRIES {
1081 ctx.set_metadata(format!("key.{i}"), "old");
1082 }
1083
1084 ctx.set_metadata("key.0", "new");
1085 assert_eq!(
1086 ctx.get_metadata("key.0"),
1087 Some("new"),
1088 "overwriting existing key at limit should succeed"
1089 );
1090 assert_eq!(
1091 ctx.filter_metadata.len(),
1092 MAX_METADATA_ENTRIES,
1093 "overwrite should not increase entry count"
1094 );
1095 }
1096
1097 #[test]
1098 fn kv_stores_lookup_with_match_types() {
1099 use praxis_core::kv::MatchType;
1100
1101 let registry = KvStoreRegistry::new();
1102 let store = registry.get_or_create("routes");
1103 store.set("route.api.v1", Arc::from("api_cluster"));
1104 store.set("route.web.main", Arc::from("web_cluster"));
1105
1106 let req = crate::test_utils::make_request(Method::GET, "/");
1107 let mut ctx = crate::test_utils::make_filter_context(&req);
1108 ctx.kv_stores = Some(®istry);
1109
1110 let store = ctx.kv_stores.unwrap().get("routes").unwrap();
1111 assert!(
1112 store.lookup("route.api", MatchType::Prefix).unwrap().is_some(),
1113 "prefix lookup should match route.api.v1"
1114 );
1115 assert!(
1116 store.lookup(".main", MatchType::Suffix).unwrap().is_some(),
1117 "suffix lookup should match route.web.main"
1118 );
1119 }
1120
1121 #[test]
1126 fn insert_and_get_filter_state_returns_typed_value() {
1127 let req = crate::test_utils::make_request(Method::GET, "/");
1128 let mut ctx = crate::test_utils::make_filter_context(&req);
1129 ctx.current_filter_id = Some(0);
1130 ctx.insert_filter_state(42_u64);
1131 assert_eq!(
1132 ctx.get_filter_state::<u64>(),
1133 Some(&42_u64),
1134 "should return the inserted value"
1135 );
1136 }
1137
1138 #[test]
1139 fn get_filter_state_returns_none_when_empty() {
1140 let req = crate::test_utils::make_request(Method::GET, "/");
1141 let mut ctx = crate::test_utils::make_filter_context(&req);
1142 ctx.current_filter_id = Some(0);
1143 assert!(
1144 ctx.get_filter_state::<u64>().is_none(),
1145 "should return None when no state stored"
1146 );
1147 }
1148
1149 #[test]
1150 fn get_filter_state_returns_none_for_wrong_type() {
1151 let req = crate::test_utils::make_request(Method::GET, "/");
1152 let mut ctx = crate::test_utils::make_filter_context(&req);
1153 ctx.current_filter_id = Some(0);
1154 ctx.insert_filter_state(42_u64);
1155 assert!(
1156 ctx.get_filter_state::<String>().is_none(),
1157 "should return None for type mismatch"
1158 );
1159 }
1160
1161 #[test]
1162 fn get_filter_state_returns_none_when_no_index() {
1163 let req = crate::test_utils::make_request(Method::GET, "/");
1164 let mut ctx = crate::test_utils::make_filter_context(&req);
1165 ctx.filter_state.insert(0, Box::new(42_u64));
1166 assert!(
1167 ctx.get_filter_state::<u64>().is_none(),
1168 "should return None when current_filter_id is None"
1169 );
1170 }
1171
1172 #[test]
1173 fn get_filter_state_mut_allows_mutation() {
1174 let req = crate::test_utils::make_request(Method::GET, "/");
1175 let mut ctx = crate::test_utils::make_filter_context(&req);
1176 ctx.current_filter_id = Some(0);
1177 ctx.insert_filter_state(10_u64);
1178 *ctx.get_filter_state_mut::<u64>().unwrap() += 5;
1179 assert_eq!(
1180 ctx.get_filter_state::<u64>(),
1181 Some(&15_u64),
1182 "mutation through get_mut should be visible"
1183 );
1184 }
1185
1186 #[test]
1187 fn remove_filter_state_takes_ownership() {
1188 let req = crate::test_utils::make_request(Method::GET, "/");
1189 let mut ctx = crate::test_utils::make_filter_context(&req);
1190 ctx.current_filter_id = Some(0);
1191 ctx.insert_filter_state("hello".to_owned());
1192 let removed = ctx.remove_filter_state::<String>();
1193 assert_eq!(removed.as_deref(), Some("hello"), "should return the stored value");
1194 assert!(
1195 ctx.get_filter_state::<String>().is_none(),
1196 "state should be gone after remove"
1197 );
1198 }
1199
1200 #[test]
1201 fn remove_filter_state_returns_none_for_wrong_type() {
1202 let req = crate::test_utils::make_request(Method::GET, "/");
1203 let mut ctx = crate::test_utils::make_filter_context(&req);
1204 ctx.current_filter_id = Some(0);
1205 ctx.insert_filter_state(42_u64);
1206 assert!(
1207 ctx.remove_filter_state::<String>().is_none(),
1208 "type mismatch should return None"
1209 );
1210 assert!(
1211 ctx.get_filter_state::<u64>().is_some(),
1212 "type mismatch remove should not destroy the entry"
1213 );
1214 }
1215
1216 #[test]
1217 fn different_indices_do_not_collide() {
1218 let req = crate::test_utils::make_request(Method::GET, "/");
1219 let mut ctx = crate::test_utils::make_filter_context(&req);
1220 ctx.current_filter_id = Some(0);
1221 ctx.insert_filter_state(100_u64);
1222 ctx.current_filter_id = Some(1);
1223 ctx.insert_filter_state(200_u64);
1224
1225 ctx.current_filter_id = Some(0);
1226 assert_eq!(ctx.get_filter_state::<u64>(), Some(&100_u64), "index 0 state");
1227
1228 ctx.current_filter_id = Some(1);
1229 assert_eq!(ctx.get_filter_state::<u64>(), Some(&200_u64), "index 1 state");
1230 }
1231
1232 #[test]
1233 fn insert_filter_state_is_noop_without_index() {
1234 let req = crate::test_utils::make_request(Method::GET, "/");
1235 let mut ctx = crate::test_utils::make_filter_context(&req);
1236 ctx.insert_filter_state(42_u64);
1237 assert!(ctx.filter_state.is_empty(), "state map should remain empty");
1238 }
1239
1240 #[test]
1245 fn matches_header_remove() {
1246 let mutation = TrustedHeaderMutation::Remove("x-dest".parse().unwrap());
1247 assert!(mutation.matches_header(&"x-dest".parse().unwrap()));
1248 assert!(!mutation.matches_header(&"x-other".parse().unwrap()));
1249 }
1250
1251 #[test]
1252 fn matches_header_set() {
1253 let mutation = TrustedHeaderMutation::Set("x-dest".parse().unwrap(), "val".parse().unwrap());
1254 assert!(mutation.matches_header(&"x-dest".parse().unwrap()));
1255 assert!(!mutation.matches_header(&"x-other".parse().unwrap()));
1256 }
1257
1258 #[test]
1259 fn matches_header_add() {
1260 let mutation = TrustedHeaderMutation::Add("x-dest".parse().unwrap(), "val".to_owned());
1261 assert!(mutation.matches_header(&"x-dest".parse().unwrap()));
1262 assert!(!mutation.matches_header(&"x-other".parse().unwrap()));
1263 }
1264
1265 #[test]
1270 fn resolve_trusted_header_empty_log() {
1271 let req = crate::test_utils::make_request(Method::GET, "/");
1272 let ctx = crate::test_utils::make_filter_context(&req);
1273 assert_eq!(
1274 ctx.resolve_trusted_header(&"x-dest".parse().unwrap()).unwrap(),
1275 None,
1276 "empty mutation log should resolve to None"
1277 );
1278 }
1279
1280 #[test]
1281 fn resolve_trusted_header_add() {
1282 let req = crate::test_utils::make_request(Method::GET, "/");
1283 let mut ctx = crate::test_utils::make_filter_context(&req);
1284 ctx.pre_read_mutations.push(TrustedHeaderMutation::Add(
1285 "x-dest".parse().unwrap(),
1286 "host:8080".to_owned(),
1287 ));
1288 assert_eq!(
1289 ctx.resolve_trusted_header(&"x-dest".parse().unwrap()).unwrap(),
1290 Some("host:8080".to_owned()),
1291 );
1292 }
1293
1294 #[test]
1295 fn resolve_trusted_header_set() {
1296 let req = crate::test_utils::make_request(Method::GET, "/");
1297 let mut ctx = crate::test_utils::make_filter_context(&req);
1298 ctx.pre_read_mutations.push(TrustedHeaderMutation::Set(
1299 "x-dest".parse().unwrap(),
1300 "host:9090".parse().unwrap(),
1301 ));
1302 assert_eq!(
1303 ctx.resolve_trusted_header(&"x-dest".parse().unwrap()).unwrap(),
1304 Some("host:9090".to_owned()),
1305 );
1306 }
1307
1308 #[test]
1309 fn resolve_trusted_header_remove_hides_earlier_add() {
1310 let req = crate::test_utils::make_request(Method::GET, "/");
1311 let mut ctx = crate::test_utils::make_filter_context(&req);
1312 ctx.pre_read_mutations.push(TrustedHeaderMutation::Add(
1313 "x-dest".parse().unwrap(),
1314 "host:8080".to_owned(),
1315 ));
1316 ctx.pre_read_mutations
1317 .push(TrustedHeaderMutation::Remove("x-dest".parse().unwrap()));
1318 assert_eq!(
1319 ctx.resolve_trusted_header(&"x-dest".parse().unwrap()).unwrap(),
1320 None,
1321 "remove after add should resolve to None"
1322 );
1323 }
1324
1325 #[test]
1326 fn resolve_trusted_header_set_overrides_add() {
1327 let req = crate::test_utils::make_request(Method::GET, "/");
1328 let mut ctx = crate::test_utils::make_filter_context(&req);
1329 ctx.pre_read_mutations.push(TrustedHeaderMutation::Add(
1330 "x-dest".parse().unwrap(),
1331 "first:8080".to_owned(),
1332 ));
1333 ctx.pre_read_mutations.push(TrustedHeaderMutation::Set(
1334 "x-dest".parse().unwrap(),
1335 "second:9090".parse().unwrap(),
1336 ));
1337 assert_eq!(
1338 ctx.resolve_trusted_header(&"x-dest".parse().unwrap()).unwrap(),
1339 Some("second:9090".to_owned()),
1340 "set after add should override"
1341 );
1342 }
1343
1344 #[test]
1345 fn resolve_trusted_header_duplicate_add_same_value_ok() {
1346 let req = crate::test_utils::make_request(Method::GET, "/");
1347 let mut ctx = crate::test_utils::make_filter_context(&req);
1348 ctx.pre_read_mutations.push(TrustedHeaderMutation::Add(
1349 "x-dest".parse().unwrap(),
1350 "host:8080".to_owned(),
1351 ));
1352 ctx.pre_read_mutations.push(TrustedHeaderMutation::Add(
1353 "x-dest".parse().unwrap(),
1354 "host:8080".to_owned(),
1355 ));
1356 assert_eq!(
1357 ctx.resolve_trusted_header(&"x-dest".parse().unwrap()).unwrap(),
1358 Some("host:8080".to_owned()),
1359 "duplicate identical adds should be allowed"
1360 );
1361 }
1362
1363 #[test]
1364 fn resolve_trusted_header_ambiguous_add_errors() {
1365 let req = crate::test_utils::make_request(Method::GET, "/");
1366 let mut ctx = crate::test_utils::make_filter_context(&req);
1367 ctx.pre_read_mutations.push(TrustedHeaderMutation::Add(
1368 "x-dest".parse().unwrap(),
1369 "host-a:8080".to_owned(),
1370 ));
1371 ctx.pre_read_mutations.push(TrustedHeaderMutation::Add(
1372 "x-dest".parse().unwrap(),
1373 "host-b:9090".to_owned(),
1374 ));
1375 let err = ctx.resolve_trusted_header(&"x-dest".parse().unwrap()).unwrap_err();
1376 assert!(
1377 err.contains("ambiguous"),
1378 "distinct adds should produce ambiguity error: {err}"
1379 );
1380 }
1381
1382 #[test]
1383 fn resolve_trusted_header_set_then_add_same_value_ok() {
1384 let req = crate::test_utils::make_request(Method::GET, "/");
1385 let mut ctx = crate::test_utils::make_filter_context(&req);
1386 ctx.pre_read_mutations.push(TrustedHeaderMutation::Set(
1387 "x-dest".parse().unwrap(),
1388 "host:8080".parse().unwrap(),
1389 ));
1390 ctx.pre_read_mutations.push(TrustedHeaderMutation::Add(
1391 "x-dest".parse().unwrap(),
1392 "host:8080".to_owned(),
1393 ));
1394 assert_eq!(
1395 ctx.resolve_trusted_header(&"x-dest".parse().unwrap()).unwrap(),
1396 Some("host:8080".to_owned()),
1397 "set then identical add should succeed"
1398 );
1399 }
1400
1401 #[test]
1402 fn resolve_trusted_header_set_then_distinct_add_errors() {
1403 let req = crate::test_utils::make_request(Method::GET, "/");
1404 let mut ctx = crate::test_utils::make_filter_context(&req);
1405 ctx.pre_read_mutations.push(TrustedHeaderMutation::Set(
1406 "x-dest".parse().unwrap(),
1407 "host-a:8080".parse().unwrap(),
1408 ));
1409 ctx.pre_read_mutations.push(TrustedHeaderMutation::Add(
1410 "x-dest".parse().unwrap(),
1411 "host-b:9090".to_owned(),
1412 ));
1413 let err = ctx.resolve_trusted_header(&"x-dest".parse().unwrap()).unwrap_err();
1414 assert!(
1415 err.contains("ambiguous"),
1416 "set then distinct add should produce ambiguity error: {err}"
1417 );
1418 }
1419
1420 #[test]
1421 fn resolve_trusted_header_temporary_ambiguity_resolved_by_remove() {
1422 let req = crate::test_utils::make_request(Method::GET, "/");
1423 let mut ctx = crate::test_utils::make_filter_context(&req);
1424 ctx.pre_read_mutations.push(TrustedHeaderMutation::Add(
1425 "x-dest".parse().unwrap(),
1426 "host-a:8080".to_owned(),
1427 ));
1428 ctx.pre_read_mutations.push(TrustedHeaderMutation::Add(
1429 "x-dest".parse().unwrap(),
1430 "host-b:9090".to_owned(),
1431 ));
1432 ctx.pre_read_mutations
1433 .push(TrustedHeaderMutation::Remove("x-dest".parse().unwrap()));
1434 assert_eq!(
1435 ctx.resolve_trusted_header(&"x-dest".parse().unwrap()).unwrap(),
1436 None,
1437 "Add(a) -> Add(b) -> Remove should resolve to None"
1438 );
1439 }
1440
1441 #[test]
1442 fn resolve_trusted_header_temporary_ambiguity_resolved_by_set() {
1443 let req = crate::test_utils::make_request(Method::GET, "/");
1444 let mut ctx = crate::test_utils::make_filter_context(&req);
1445 ctx.pre_read_mutations.push(TrustedHeaderMutation::Add(
1446 "x-dest".parse().unwrap(),
1447 "host-a:8080".to_owned(),
1448 ));
1449 ctx.pre_read_mutations.push(TrustedHeaderMutation::Add(
1450 "x-dest".parse().unwrap(),
1451 "host-b:9090".to_owned(),
1452 ));
1453 ctx.pre_read_mutations.push(TrustedHeaderMutation::Set(
1454 "x-dest".parse().unwrap(),
1455 "final:7070".parse().unwrap(),
1456 ));
1457 assert_eq!(
1458 ctx.resolve_trusted_header(&"x-dest".parse().unwrap()).unwrap(),
1459 Some("final:7070".to_owned()),
1460 "Add(a) -> Add(b) -> Set(c) should resolve to c"
1461 );
1462 }
1463
1464 #[test]
1465 fn resolve_trusted_header_remove_then_set_produces_set() {
1466 let req = crate::test_utils::make_request(Method::GET, "/");
1467 let mut ctx = crate::test_utils::make_filter_context(&req);
1468 ctx.pre_read_mutations.push(TrustedHeaderMutation::Add(
1469 "x-dest".parse().unwrap(),
1470 "old:8080".to_owned(),
1471 ));
1472 ctx.pre_read_mutations
1473 .push(TrustedHeaderMutation::Remove("x-dest".parse().unwrap()));
1474 ctx.pre_read_mutations.push(TrustedHeaderMutation::Set(
1475 "x-dest".parse().unwrap(),
1476 "new:9090".parse().unwrap(),
1477 ));
1478 assert_eq!(
1479 ctx.resolve_trusted_header(&"x-dest".parse().unwrap()).unwrap(),
1480 Some("new:9090".to_owned()),
1481 "remove then set should produce the set value"
1482 );
1483 }
1484
1485 #[test]
1490 fn pending_header_value_empty() {
1491 let req = crate::test_utils::make_request(Method::GET, "/");
1492 let ctx = crate::test_utils::make_filter_context(&req);
1493 assert_eq!(
1494 ctx.pending_header_value(&"x-dest".parse().unwrap()).unwrap(),
1495 PendingHeaderResult::Absent,
1496 "no pending mutations should resolve to Absent"
1497 );
1498 }
1499
1500 #[test]
1501 fn pending_header_value_from_set() {
1502 let req = crate::test_utils::make_request(Method::GET, "/");
1503 let mut ctx = crate::test_utils::make_filter_context(&req);
1504 ctx.request_headers_to_set
1505 .push(("x-dest".parse().unwrap(), "set-val:9090".parse().unwrap()));
1506 assert_eq!(
1507 ctx.pending_header_value(&"x-dest".parse().unwrap()).unwrap(),
1508 PendingHeaderResult::Value("set-val:9090".to_owned()),
1509 );
1510 }
1511
1512 #[test]
1513 fn pending_header_value_from_extra() {
1514 let req = crate::test_utils::make_request(Method::GET, "/");
1515 let mut ctx = crate::test_utils::make_filter_context(&req);
1516 ctx.extra_request_headers
1517 .push((Cow::Borrowed("x-dest"), "extra-val:7070".to_owned()));
1518 assert_eq!(
1519 ctx.pending_header_value(&"x-dest".parse().unwrap()).unwrap(),
1520 PendingHeaderResult::Value("extra-val:7070".to_owned()),
1521 );
1522 }
1523
1524 #[test]
1525 fn pending_header_value_set_after_remove_produces_set_value() {
1526 let req = crate::test_utils::make_request(Method::GET, "/");
1527 let mut ctx = crate::test_utils::make_filter_context(&req);
1528 ctx.request_headers_to_remove.push("x-dest".parse().unwrap());
1529 ctx.request_headers_to_set
1530 .push(("x-dest".parse().unwrap(), "set-val:9090".parse().unwrap()));
1531 assert_eq!(
1532 ctx.pending_header_value(&"x-dest".parse().unwrap()).unwrap(),
1533 PendingHeaderResult::Value("set-val:9090".to_owned()),
1534 "set after remove should produce the set value"
1535 );
1536 }
1537
1538 #[test]
1539 fn pending_header_value_remove_without_set_is_removed() {
1540 let req = crate::test_utils::make_request(Method::GET, "/");
1541 let mut ctx = crate::test_utils::make_filter_context(&req);
1542 ctx.request_headers_to_remove.push("x-dest".parse().unwrap());
1543 assert_eq!(
1544 ctx.pending_header_value(&"x-dest".parse().unwrap()).unwrap(),
1545 PendingHeaderResult::Removed,
1546 "remove without subsequent set should resolve to Removed"
1547 );
1548 }
1549
1550 #[test]
1551 fn pending_header_value_distinct_extras_error() {
1552 let req = crate::test_utils::make_request(Method::GET, "/");
1553 let mut ctx = crate::test_utils::make_filter_context(&req);
1554 ctx.extra_request_headers
1555 .push((Cow::Borrowed("x-dest"), "val-a:7070".to_owned()));
1556 ctx.extra_request_headers
1557 .push((Cow::Borrowed("x-dest"), "val-b:8080".to_owned()));
1558 let err = ctx.pending_header_value(&"x-dest".parse().unwrap()).unwrap_err();
1559 assert!(err.contains("ambiguous"), "distinct extras should error: {err}");
1560 }
1561
1562 #[test]
1567 fn structured_metadata_absent_by_default() {
1568 let req = crate::test_utils::make_request(Method::GET, "/");
1569 let ctx = crate::test_utils::make_filter_context(&req);
1570 assert!(
1571 ctx.get_structured_metadata("ns", "key").is_none(),
1572 "structured_metadata should be empty by default"
1573 );
1574 }
1575
1576 #[test]
1577 fn set_and_get_structured_metadata() {
1578 let req = crate::test_utils::make_request(Method::GET, "/");
1579 let mut ctx = crate::test_utils::make_filter_context(&req);
1580 ctx.set_structured_metadata("test_filter", "model", serde_json::json!("gpt-4"));
1581 assert_eq!(
1582 ctx.get_structured_metadata("test_filter", "model"),
1583 Some(&serde_json::json!("gpt-4")),
1584 "get should return the value set by set_structured_metadata"
1585 );
1586 }
1587
1588 #[test]
1589 fn merge_structured_metadata_overwrites_existing() {
1590 let req = crate::test_utils::make_request(Method::GET, "/");
1591 let mut ctx = crate::test_utils::make_filter_context(&req);
1592 ctx.set_structured_metadata("ns", "key", serde_json::json!("old"));
1593 let mut merge = serde_json::Map::new();
1594 merge.insert("key".to_owned(), serde_json::json!("new"));
1595 merge.insert("extra".to_owned(), serde_json::json!(42));
1596 ctx.merge_structured_metadata("ns", merge);
1597 assert_eq!(
1598 ctx.get_structured_metadata("ns", "key"),
1599 Some(&serde_json::json!("new")),
1600 "merge should overwrite existing key"
1601 );
1602 assert_eq!(
1603 ctx.get_structured_metadata("ns", "extra"),
1604 Some(&serde_json::json!(42)),
1605 "merge should add new key"
1606 );
1607 }
1608
1609 #[test]
1610 fn structured_metadata_key_limit_enforced() {
1611 let req = crate::test_utils::make_request(Method::GET, "/");
1612 let mut ctx = crate::test_utils::make_filter_context(&req);
1613 for i in 0..MAX_STRUCTURED_METADATA_KEYS {
1614 ctx.set_structured_metadata("ns", &format!("key-{i}"), serde_json::json!(i));
1615 }
1616 assert_eq!(
1617 ctx.get_structured_metadata("ns", "key-0"),
1618 Some(&serde_json::json!(0)),
1619 "first key should exist"
1620 );
1621
1622 ctx.set_structured_metadata("ns", "overflow", serde_json::json!("dropped"));
1623 assert!(
1624 ctx.get_structured_metadata("ns", "overflow").is_none(),
1625 "key beyond limit should be dropped"
1626 );
1627
1628 ctx.set_structured_metadata("ns", "key-0", serde_json::json!("updated"));
1629 assert_eq!(
1630 ctx.get_structured_metadata("ns", "key-0"),
1631 Some(&serde_json::json!("updated")),
1632 "existing key can still be overwritten past limit"
1633 );
1634 }
1635
1636 #[test]
1637 fn merge_structured_metadata_respects_key_limit() {
1638 let req = crate::test_utils::make_request(Method::GET, "/");
1639 let mut ctx = crate::test_utils::make_filter_context(&req);
1640 for i in 0..MAX_STRUCTURED_METADATA_KEYS {
1641 ctx.set_structured_metadata("ns", &format!("key-{i}"), serde_json::json!(i));
1642 }
1643
1644 let mut merge = serde_json::Map::new();
1645 merge.insert("key-0".to_owned(), serde_json::json!("overwritten"));
1646 merge.insert("new-key".to_owned(), serde_json::json!("dropped"));
1647 ctx.merge_structured_metadata("ns", merge);
1648
1649 assert_eq!(
1650 ctx.get_structured_metadata("ns", "key-0"),
1651 Some(&serde_json::json!("overwritten")),
1652 "merge should overwrite existing key past limit"
1653 );
1654 assert!(
1655 ctx.get_structured_metadata("ns", "new-key").is_none(),
1656 "merge should drop new key past limit"
1657 );
1658 }
1659}