1use std::{collections::VecDeque, net::IpAddr, sync::Arc, time::Instant};
7
8use bytes::Bytes;
9use praxis_core::connectivity::Upstream;
10use praxis_filter::{BodyBuffer, BodyMode, FilterPipeline, Request, Response, TrustedHeaderMutation};
11use tokio::sync::OwnedSemaphorePermit;
12use tracing::Span;
13
14#[expect(clippy::struct_excessive_bools, reason = "lifecycle flags")]
30pub struct PingoraRequestCtx {
31 pub _connection_permit: Option<OwnedSemaphorePermit>,
37
38 pub _global_connection_permit: Option<OwnedSemaphorePermit>,
42
43 pub client_addr: Option<IpAddr>,
45
46 pub client_http_version: Option<http::Version>,
51
52 pub cluster: Option<Arc<str>>,
54
55 pub cached_body_done_indices: Vec<bool>,
61
62 pub cached_executed_filter_indices: Vec<bool>,
67
68 pub downstream_tls: bool,
75
76 pub peer_identity: Option<praxis_tls::TlsPeerIdentity>,
84
85 pub connection_upgraded: bool,
91
92 pub extensions: praxis_filter::RequestExtensions,
99
100 pub filter_metadata: std::collections::HashMap<String, String>,
106
107 pub pre_read_mutations: Vec<TrustedHeaderMutation>,
113
114 pub structured_metadata: std::collections::HashMap<String, serde_json::Value>,
120
121 pub mutated_request_body_len: Option<usize>,
127
128 pub pinned_pipeline: Option<Arc<FilterPipeline>>,
138
139 pub filter_results: std::collections::HashMap<&'static str, praxis_filter::FilterResultSet>,
145
146 pub filter_state: std::collections::HashMap<usize, Box<dyn std::any::Any + Send + Sync>>,
156
157 pub metrics_cluster: Option<Arc<str>>,
161
162 pub metrics_cluster_shared: Option<::metrics::SharedString>,
169
170 pub metrics_route: Option<::metrics::SharedString>,
172
173 pub(crate) _active_connection: Option<crate::http::pingora::metrics::ActiveConnectionGuard>,
175
176 pub upstream_connect_start: Option<Instant>,
178
179 pub pre_read_body: Option<VecDeque<Bytes>>,
187
188 pub request_body_buffer: Option<BodyBuffer>,
192
193 pub request_body_bytes: u64,
195
196 pub request_body_mode: BodyMode,
200
201 pub request_body_released: bool,
204
205 pub request_is_idempotent: bool,
207
208 pub request_snapshot: Option<Request>,
210
211 pub request_span: Span,
218
219 pub request_start: Instant,
221
222 pub response_body_buffer: Option<BodyBuffer>,
226
227 pub response_body_bytes: u64,
229
230 pub response_body_mode: BodyMode,
234
235 pub response_body_released: bool,
237
238 pub response_header_snapshot: Option<Response>,
244
245 pub upstream_response_status: Option<u16>,
248
249 pub response_phase_done: bool,
253
254 pub retries: u32,
256
257 pub selected_endpoint_index: Option<usize>,
261
262 pub rewritten_path: Option<String>,
269
270 pub upstream: Option<Upstream>,
272
273 pub upstream_for_retry: Option<Upstream>,
275}
276
277macro_rules! filter_context {
287 ($ctx:expr, $pipeline:expr, $request:expr, $response_header:expr) => {{
288 $pipeline.prepare_extensions(&mut $ctx.extensions);
289 praxis_filter::HttpFilterContext {
290 buffered_request_body: $ctx
291 .pre_read_body
292 .as_ref()
293 .map(|chunks| chunks.front().cloned().unwrap_or_default()),
294 body_done_indices: std::mem::take(&mut $ctx.cached_body_done_indices),
295 branch_iterations: std::collections::HashMap::new(),
296 client_addr: $ctx.client_addr,
297 cluster: $ctx.cluster.take(),
298 current_filter_id: None,
299 downstream_tls: $ctx.downstream_tls,
300 metrics_route: $ctx.metrics_route.clone(),
301 peer_identity: $ctx.peer_identity.clone(),
302 extensions: std::mem::take(&mut $ctx.extensions),
303 executed_filter_indices: std::mem::take(&mut $ctx.cached_executed_filter_indices),
304 extra_request_headers: Vec::new(),
305 request_headers_to_remove: Vec::new(),
306 request_headers_to_set: Vec::new(),
307 filter_metadata: std::mem::take(&mut $ctx.filter_metadata),
308 pre_read_mutations: std::mem::take(&mut $ctx.pre_read_mutations),
309 structured_metadata: std::mem::take(&mut $ctx.structured_metadata),
310 filter_results: std::mem::take(&mut $ctx.filter_results),
311 filter_state: std::mem::take(&mut $ctx.filter_state),
312 health_registry: $pipeline.health_registry(),
313 id_generator: $pipeline.id_generator(),
314 kv_stores: $pipeline.kv_stores(),
315 subrequest_client: $pipeline.subrequest_client(),
316 subrequest_response_mode: praxis_filter::SubRequestResponseMode::Buffered,
317 request: $request,
318 request_body_bytes: $ctx.request_body_bytes,
319 request_body_mode: $ctx.request_body_mode,
320 request_start: $ctx.request_start,
321 response_body_bytes: $ctx.response_body_bytes,
322 response_body_mode: $ctx.response_body_mode,
323 response_header: $response_header,
324 response_headers_modified: false,
325 rewritten_path: $ctx.rewritten_path.take(),
326 selected_endpoint_index: $ctx.selected_endpoint_index,
327 time_source: $pipeline.time_source(),
328 upstream: $ctx.upstream.take(),
329 }
330 }};
331}
332
333impl PingoraRequestCtx {
334 pub fn build_filter_context<'a>(
358 &mut self,
359 pipeline: &'a FilterPipeline,
360 request: &'a Request,
361 response_header: Option<&'a mut Response>,
362 ) -> praxis_filter::HttpFilterContext<'a> {
363 filter_context!(self, pipeline, request, response_header)
364 }
365
366 pub fn filter_context_for<'a>(
393 &'a mut self,
394 pipeline: &'a FilterPipeline,
395 response_header: Option<&'a mut Response>,
396 ) -> Option<praxis_filter::HttpFilterContext<'a>> {
397 let request = self.request_snapshot.as_ref()?;
398 Some(filter_context!(self, pipeline, request, response_header))
399 }
400
401 pub fn response_body_context_for<'a>(
409 &'a mut self,
410 pipeline: &'a FilterPipeline,
411 ) -> Option<(praxis_filter::HttpFilterContext<'a>, Option<&'a Response>)> {
412 let request = self.request_snapshot.as_ref()?;
413 let response_header = self.response_header_snapshot.as_ref();
414 Some((filter_context!(self, pipeline, request, None), response_header))
415 }
416
417 pub fn pin_pipeline(&mut self, swap: &arc_swap::ArcSwap<FilterPipeline>) -> Arc<FilterPipeline> {
430 if let Some(existing) = &self.pinned_pipeline {
431 return Arc::clone(existing);
432 }
433 let pipeline = swap.load_full();
434 self.pinned_pipeline = Some(Arc::clone(&pipeline));
435 pipeline
436 }
437
438 pub fn pipeline(&self, swap: &arc_swap::ArcSwap<FilterPipeline>) -> Arc<FilterPipeline> {
451 self.pinned_pipeline
452 .as_ref()
453 .map_or_else(|| swap.load_full(), Arc::clone)
454 }
455}
456
457impl Default for PingoraRequestCtx {
458 #[expect(
459 clippy::too_many_lines,
460 reason = "context default enumerates all lifecycle fields explicitly"
461 )]
462 fn default() -> Self {
463 Self {
464 _connection_permit: None,
465 _global_connection_permit: None,
466 cached_body_done_indices: Vec::new(),
467 cached_executed_filter_indices: Vec::new(),
468 client_addr: None,
469 client_http_version: None,
470 cluster: None,
471 connection_upgraded: false,
472 downstream_tls: false,
473 peer_identity: None,
474 extensions: praxis_filter::RequestExtensions::new(),
475 filter_metadata: std::collections::HashMap::new(),
476 pre_read_mutations: Vec::new(),
477 structured_metadata: std::collections::HashMap::new(),
478 mutated_request_body_len: None,
479 pinned_pipeline: None,
480 filter_results: std::collections::HashMap::new(),
481 filter_state: std::collections::HashMap::new(),
482 metrics_cluster: None,
483 metrics_cluster_shared: None,
484 metrics_route: None,
485 _active_connection: None,
486 upstream_connect_start: None,
487 pre_read_body: None,
488 request_body_buffer: None,
489 request_body_bytes: 0,
490 request_body_mode: BodyMode::Stream,
491 request_body_released: false,
492 request_is_idempotent: false,
493 request_snapshot: None,
494 request_span: Span::none(),
495 request_start: Instant::now(),
496 response_body_buffer: None,
497 response_body_bytes: 0,
498 response_body_mode: BodyMode::Stream,
499 response_body_released: false,
500 response_header_snapshot: None,
501 upstream_response_status: None,
502 response_phase_done: false,
503 retries: 0,
504 rewritten_path: None,
505 selected_endpoint_index: None,
506 upstream: None,
507 upstream_for_retry: None,
508 }
509 }
510}
511
512#[cfg(test)]
517#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
518#[allow(
519 clippy::unwrap_used,
520 clippy::expect_used,
521 clippy::indexing_slicing,
522 clippy::significant_drop_tightening,
523 clippy::too_many_lines,
524 reason = "tests"
525)]
526mod tests {
527 use std::net::Ipv4Addr;
528
529 use http::{HeaderMap, Method, Uri};
530 use praxis_filter::FilterRegistry;
531
532 use super::*;
533
534 #[test]
535 fn default_state_has_no_client_addr() {
536 let ctx = default_ctx();
537 assert!(ctx.client_addr.is_none(), "default client_addr should be None");
538 }
539
540 #[test]
541 fn default_state_has_no_cluster() {
542 let ctx = default_ctx();
543 assert!(ctx.cluster.is_none(), "default cluster should be None");
544 }
545
546 #[test]
547 fn default_state_has_zero_retries() {
548 let ctx = default_ctx();
549 assert_eq!(ctx.retries, 0, "default retries should be zero");
550 }
551
552 #[test]
553 fn default_state_flags_are_false() {
554 let ctx = default_ctx();
555 assert!(
556 !ctx.request_body_released,
557 "default request_body_released should be false"
558 );
559 assert!(
560 !ctx.response_body_released,
561 "default response_body_released should be false"
562 );
563 assert!(
564 !ctx.request_is_idempotent,
565 "default request_is_idempotent should be false"
566 );
567 assert!(!ctx.response_phase_done, "default response_phase_done should be false");
568 }
569
570 #[test]
571 fn default_state_buffers_are_none() {
572 let ctx = default_ctx();
573 assert!(
574 ctx.request_body_buffer.is_none(),
575 "default request_body_buffer should be None"
576 );
577 assert!(
578 ctx.response_body_buffer.is_none(),
579 "default response_body_buffer should be None"
580 );
581 assert!(ctx.pre_read_body.is_none(), "default pre_read_body should be None");
582 }
583
584 #[test]
585 fn default_state_request_span_is_disabled() {
586 let ctx = default_ctx();
587 assert!(
588 ctx.request_span.is_disabled(),
589 "default request_span should be a disabled (none) span"
590 );
591 }
592
593 #[test]
594 fn default_state_snapshots_are_none() {
595 let ctx = default_ctx();
596 assert!(
597 ctx.request_snapshot.is_none(),
598 "default request_snapshot should be None"
599 );
600 assert!(ctx.upstream.is_none(), "default upstream should be None");
601 assert!(
602 ctx.upstream_for_retry.is_none(),
603 "default upstream_for_retry should be None"
604 );
605 }
606
607 #[test]
608 fn set_client_addr() {
609 let mut ctx = default_ctx();
610 let addr = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100));
611 ctx.client_addr = Some(addr);
612 assert_eq!(
613 ctx.client_addr.unwrap(),
614 addr,
615 "client_addr should match assigned value"
616 );
617 }
618
619 #[test]
620 fn set_cluster() {
621 let mut ctx = default_ctx();
622 ctx.cluster = Some(Arc::from("api-cluster"));
623 assert_eq!(
624 ctx.cluster.as_deref(),
625 Some("api-cluster"),
626 "cluster should match assigned value"
627 );
628 }
629
630 #[test]
631 fn set_upstream() {
632 let mut ctx = default_ctx();
633 let upstream = Upstream {
634 address: Arc::from("10.0.0.1:80"),
635 tls: None,
636 connection: Arc::new(praxis_core::connectivity::ConnectionOptions::default()),
637 };
638 ctx.upstream = Some(upstream.clone());
639 assert_eq!(
640 &*ctx.upstream.as_ref().unwrap().address,
641 "10.0.0.1:80",
642 "upstream address should match assigned value"
643 );
644 }
645
646 #[test]
647 fn increment_retries() {
648 let mut ctx = default_ctx();
649 ctx.retries += 1;
650 ctx.retries += 1;
651 assert_eq!(ctx.retries, 2, "retries should be 2 after two increments");
652 }
653
654 #[test]
655 fn release_request_body_flag() {
656 let mut ctx = default_ctx();
657 assert!(!ctx.request_body_released, "request_body_released should start false");
658 ctx.request_body_released = true;
659 assert!(
660 ctx.request_body_released,
661 "request_body_released should be true after setting"
662 );
663 }
664
665 #[test]
666 fn release_response_body_flag() {
667 let mut ctx = default_ctx();
668 assert!(!ctx.response_body_released, "response_body_released should start false");
669 ctx.response_body_released = true;
670 assert!(
671 ctx.response_body_released,
672 "response_body_released should be true after setting"
673 );
674 }
675
676 #[test]
677 fn response_phase_done_flag() {
678 let mut ctx = default_ctx();
679 assert!(!ctx.response_phase_done, "response_phase_done should start false");
680 ctx.response_phase_done = true;
681 assert!(
682 ctx.response_phase_done,
683 "response_phase_done should be true after setting"
684 );
685 }
686
687 #[test]
688 fn set_pre_read_body() {
689 let mut ctx = default_ctx();
690 let chunks = VecDeque::from([Bytes::from_static(b"chunk1"), Bytes::from_static(b"chunk2")]);
691 ctx.pre_read_body = Some(chunks);
692 let body = ctx.pre_read_body.as_ref().unwrap();
693 assert_eq!(body.len(), 2, "pre_read_body should contain 2 chunks");
694 assert_eq!(body[0], Bytes::from_static(b"chunk1"), "first chunk should be 'chunk1'");
695 assert_eq!(
696 body[1],
697 Bytes::from_static(b"chunk2"),
698 "second chunk should be 'chunk2'"
699 );
700 }
701
702 #[test]
703 fn set_request_snapshot() {
704 let mut ctx = default_ctx();
705 let snapshot = Request {
706 method: Method::POST,
707 uri: "/api/data".parse::<Uri>().unwrap(),
708 headers: HeaderMap::new(),
709 };
710 ctx.request_snapshot = Some(snapshot);
711 let snap = ctx.request_snapshot.as_ref().unwrap();
712 assert_eq!(snap.method, Method::POST, "snapshot method should be POST");
713 assert_eq!(snap.uri.path(), "/api/data", "snapshot URI path should be /api/data");
714 }
715
716 #[test]
717 fn request_body_buffer_lifecycle() {
718 let mut ctx = default_ctx();
719 let mut buf = BodyBuffer::new(100);
720 buf.push(Bytes::from_static(b"data")).unwrap();
721 ctx.request_body_buffer = Some(buf);
722
723 assert!(
724 ctx.request_body_buffer.is_some(),
725 "buffer should be present after assignment"
726 );
727 let taken = ctx.request_body_buffer.take().unwrap();
728 assert_eq!(
729 taken.freeze(),
730 Bytes::from_static(b"data"),
731 "frozen buffer should contain pushed data"
732 );
733 assert!(ctx.request_body_buffer.is_none(), "buffer should be None after take");
734 }
735
736 #[test]
737 fn default_request_body_mode_is_stream() {
738 let ctx = default_ctx();
739 assert_eq!(
740 ctx.request_body_mode,
741 BodyMode::Stream,
742 "default request_body_mode should be Stream"
743 );
744 }
745
746 #[test]
747 fn default_response_body_mode_is_stream() {
748 let ctx = default_ctx();
749 assert_eq!(
750 ctx.response_body_mode,
751 BodyMode::Stream,
752 "default response_body_mode should be Stream"
753 );
754 }
755
756 #[test]
757 fn set_request_body_mode() {
758 let mut ctx = default_ctx();
759 ctx.request_body_mode = BodyMode::StreamBuffer { max_bytes: Some(4096) };
760 assert_eq!(
761 ctx.request_body_mode,
762 BodyMode::StreamBuffer { max_bytes: Some(4096) },
763 "request_body_mode should match assigned value"
764 );
765 }
766
767 #[test]
768 fn set_response_body_mode() {
769 let mut ctx = default_ctx();
770 ctx.response_body_mode = BodyMode::StreamBuffer { max_bytes: Some(8192) };
771 assert_eq!(
772 ctx.response_body_mode,
773 BodyMode::StreamBuffer { max_bytes: Some(8192) },
774 "response_body_mode should match assigned value"
775 );
776 }
777
778 #[test]
783 fn metadata_roundtrip_through_filter_context() {
784 let registry = FilterRegistry::with_builtins();
785 let pipeline = FilterPipeline::build(&mut [], ®istry).unwrap();
786 let request = Request {
787 method: Method::GET,
788 uri: "/".parse::<Uri>().unwrap(),
789 headers: HeaderMap::new(),
790 };
791
792 let mut ctx = default_ctx();
793 ctx.filter_metadata.insert("rpc.method".to_owned(), "echo".to_owned());
794 ctx.filter_metadata.insert("rpc.status".to_owned(), "ok".to_owned());
795
796 let fctx = ctx.build_filter_context(&pipeline, &request, None);
797 assert_eq!(
798 fctx.get_metadata("rpc.method"),
799 Some("echo"),
800 "metadata written before build should survive into filter context"
801 );
802 assert_eq!(
803 fctx.get_metadata("rpc.status"),
804 Some("ok"),
805 "multiple metadata keys should round-trip"
806 );
807 }
808
809 #[test]
810 fn metadata_written_in_filter_context_persists_back() {
811 let registry = FilterRegistry::with_builtins();
812 let pipeline = FilterPipeline::build(&mut [], ®istry).unwrap();
813 let request = Request {
814 method: Method::GET,
815 uri: "/".parse::<Uri>().unwrap(),
816 headers: HeaderMap::new(),
817 };
818
819 let mut ctx = default_ctx();
820 let mut fctx = ctx.build_filter_context(&pipeline, &request, None);
821 fctx.set_metadata("trace.id", "abc-123");
822 fctx.set_metadata("trace.span", "42");
823
824 ctx.filter_metadata = fctx.filter_metadata;
825 assert_eq!(
826 ctx.filter_metadata.get("trace.id").map(String::as_str),
827 Some("abc-123"),
828 "metadata set in filter context should persist back to protocol context"
829 );
830 assert_eq!(
831 ctx.filter_metadata.get("trace.span").map(String::as_str),
832 Some("42"),
833 "multiple metadata keys should persist back"
834 );
835 }
836
837 #[test]
842 fn pin_pipeline_captures_current_arc() {
843 let registry = FilterRegistry::with_builtins();
844 let pipeline_a = Arc::new(FilterPipeline::build(&mut [], ®istry).unwrap());
845 let swap = arc_swap::ArcSwap::from(Arc::clone(&pipeline_a));
846
847 let mut ctx = default_ctx();
848 let pinned = ctx.pin_pipeline(&swap);
849 assert!(
850 Arc::ptr_eq(&pinned, &pipeline_a),
851 "pin_pipeline should return the current pipeline"
852 );
853 assert!(
854 Arc::ptr_eq(ctx.pinned_pipeline.as_ref().unwrap(), &pipeline_a),
855 "pinned_pipeline should be stored in ctx"
856 );
857 }
858
859 #[test]
860 fn pipeline_returns_pinned_after_reload() {
861 let registry = FilterRegistry::with_builtins();
862 let pipeline_a = Arc::new(FilterPipeline::build(&mut [], ®istry).unwrap());
863 let swap = arc_swap::ArcSwap::from(Arc::clone(&pipeline_a));
864
865 let mut ctx = default_ctx();
866 ctx.pin_pipeline(&swap);
867
868 let pipeline_b = Arc::new(FilterPipeline::build(&mut [], ®istry).unwrap());
869 swap.store(pipeline_b);
870
871 let later = ctx.pipeline(&swap);
872 assert!(
873 Arc::ptr_eq(&later, &pipeline_a),
874 "later hooks should still return pipeline A after reload"
875 );
876 }
877
878 #[test]
879 fn new_request_after_reload_pins_new_pipeline() {
880 let registry = FilterRegistry::with_builtins();
881 let pipeline_a = Arc::new(FilterPipeline::build(&mut [], ®istry).unwrap());
882 let swap = arc_swap::ArcSwap::from(Arc::clone(&pipeline_a));
883
884 let mut ctx_a = default_ctx();
885 ctx_a.pin_pipeline(&swap);
886
887 let pipeline_b = Arc::new(FilterPipeline::build(&mut [], ®istry).unwrap());
888 swap.store(Arc::clone(&pipeline_b));
889
890 let mut ctx_b = default_ctx();
891 ctx_b.pin_pipeline(&swap);
892
893 assert!(
894 Arc::ptr_eq(&ctx_a.pipeline(&swap), &pipeline_a),
895 "request A should use old pipeline"
896 );
897 assert!(
898 Arc::ptr_eq(&ctx_b.pipeline(&swap), &pipeline_b),
899 "request B should use new pipeline"
900 );
901 }
902
903 #[test]
904 fn old_pipeline_drops_after_ctx_drops() {
905 let registry = FilterRegistry::with_builtins();
906 let pipeline_a = Arc::new(FilterPipeline::build(&mut [], ®istry).unwrap());
907 let weak_a = Arc::downgrade(&pipeline_a);
908 let swap = arc_swap::ArcSwap::from(pipeline_a);
909
910 let mut ctx = default_ctx();
911 ctx.pin_pipeline(&swap);
912
913 swap.store(Arc::new(FilterPipeline::build(&mut [], ®istry).unwrap()));
914
915 assert!(weak_a.upgrade().is_some(), "old pipeline alive while ctx exists");
916
917 drop(ctx);
918
919 assert!(weak_a.upgrade().is_none(), "old pipeline drops with ctx");
920 }
921
922 #[test]
923 fn pipeline_helper_returns_pinned_for_every_phase() {
924 let registry = FilterRegistry::with_builtins();
925 let pipeline_a = Arc::new(FilterPipeline::build(&mut [], ®istry).unwrap());
926 let swap = arc_swap::ArcSwap::from(Arc::clone(&pipeline_a));
927
928 let mut ctx = default_ctx();
929 ctx.pin_pipeline(&swap);
930
931 swap.store(Arc::new(FilterPipeline::build(&mut [], ®istry).unwrap()));
932
933 for phase in ["request_body", "response", "response_body", "logging"] {
934 let p = ctx.pipeline(&swap);
935 assert!(
936 Arc::ptr_eq(&p, &pipeline_a),
937 "{phase}: should still return pinned pipeline A"
938 );
939 }
940 }
941
942 #[test]
943 fn pipeline_fallback_when_not_pinned() {
944 let registry = FilterRegistry::with_builtins();
945 let pipeline_a = Arc::new(FilterPipeline::build(&mut [], ®istry).unwrap());
946 let swap = arc_swap::ArcSwap::from(Arc::clone(&pipeline_a));
947
948 let ctx = default_ctx();
949
950 let loaded = ctx.pipeline(&swap);
951 assert!(
952 Arc::ptr_eq(&loaded, &pipeline_a),
953 "unpinned ctx should fall back to current ArcSwap value"
954 );
955 }
956
957 #[test]
958 fn pin_pipeline_is_idempotent_after_reload() {
959 let registry = FilterRegistry::with_builtins();
960 let pipeline_a = Arc::new(FilterPipeline::build(&mut [], ®istry).unwrap());
961 let swap = arc_swap::ArcSwap::from(Arc::clone(&pipeline_a));
962
963 let mut ctx = default_ctx();
964 ctx.pin_pipeline(&swap);
965
966 let pipeline_b = Arc::new(FilterPipeline::build(&mut [], ®istry).unwrap());
967 swap.store(pipeline_b);
968
969 let second_pin = ctx.pin_pipeline(&swap);
970 assert!(
971 Arc::ptr_eq(&second_pin, &pipeline_a),
972 "repeated pin_pipeline after reload should return the original pin"
973 );
974 }
975
976 #[test]
977 fn filter_state_isolated_across_pipelines_with_same_ids() {
978 let registry = FilterRegistry::with_builtins();
979 let pipeline_a = Arc::new(FilterPipeline::build(&mut [], ®istry).unwrap());
980 let pipeline_b = Arc::new(FilterPipeline::build(&mut [], ®istry).unwrap());
981
982 let request = Request {
983 method: Method::GET,
984 uri: "/".parse::<Uri>().unwrap(),
985 headers: HeaderMap::new(),
986 };
987
988 let mut ctx_a = default_ctx();
989 ctx_a.pinned_pipeline = Some(Arc::clone(&pipeline_a));
990 ctx_a.request_snapshot = Some(request.clone());
991 let mut fctx_a = ctx_a.build_filter_context(&pipeline_a, &request, None);
992 fctx_a.current_filter_id = Some(0);
993 fctx_a.insert_filter_state(String::from("from_pipeline_a"));
994 ctx_a.filter_state = fctx_a.filter_state;
995
996 let mut ctx_b = default_ctx();
997 ctx_b.pinned_pipeline = Some(Arc::clone(&pipeline_b));
998 ctx_b.request_snapshot = Some(request.clone());
999 let fctx_b = ctx_b.build_filter_context(&pipeline_b, &request, None);
1000
1001 assert!(
1002 fctx_b.filter_state.is_empty(),
1003 "request B should have its own empty state map"
1004 );
1005
1006 let fctx_a2 = ctx_a.filter_context_for(&pipeline_a, None).unwrap();
1007 assert_eq!(
1008 fctx_a2.filter_state.get(&0).and_then(|v| v.downcast_ref::<String>()),
1009 Some(&String::from("from_pipeline_a")),
1010 "request A should still see its own state in a later phase"
1011 );
1012 }
1013
1014 fn default_ctx() -> PingoraRequestCtx {
1020 PingoraRequestCtx::default()
1021 }
1022}