1#![cfg(feature = "alloc")]
2
3use alloc::collections::BTreeMap;
4use alloc::string::String;
5use alloc::string::ToString;
6use alloc::sync::Arc;
7use alloc::vec::Vec;
8
9use bytes::Bytes;
10#[cfg(feature = "std")]
11use proxima_core::signal::Signal;
12#[cfg(feature = "std")]
13use std::sync::OnceLock;
14
15use proxima_core::time::Instant;
16
17use crate::pipe::body::{ChunkStream, RequestStream, ResponseStream};
18use crate::pipe::capture_surface::CaptureContext;
19use crate::pipe::endpoint::PeerInfo;
20use crate::pipe::header_list::HeaderList;
21use crate::pipe::header_name::HeaderName;
22use crate::pipe::telemetry_surface::{Labels, NoopTelemetry, TelemetryHandle};
23#[cfg(feature = "std")]
24use crate::pipe::upgrade::UpgradeHandler;
25use proxima_core::ProximaError;
26
27#[cfg(feature = "std")]
37static NOOP_TELEMETRY: OnceLock<TelemetryHandle> = OnceLock::new();
38#[cfg(feature = "std")]
39static NEVER_CANCEL: OnceLock<Signal> = OnceLock::new();
40
41#[cfg(feature = "std")]
42fn noop_telemetry() -> TelemetryHandle {
43 NOOP_TELEMETRY
44 .get_or_init(|| Arc::new(NoopTelemetry))
45 .clone()
46}
47
48#[cfg(not(feature = "std"))]
49fn noop_telemetry() -> TelemetryHandle {
50 Arc::new(NoopTelemetry)
51}
52
53#[cfg(feature = "std")]
54fn never_cancel() -> Signal {
55 NEVER_CANCEL.get_or_init(Signal::new).clone()
56}
57
58#[derive(Clone)]
59pub struct RequestContext {
60 pub telemetry: TelemetryHandle,
61 pub deadline: Option<Instant>,
65 pub trace_id: Option<Arc<[u8]>>,
71 pub baggage: Option<Arc<[u8]>>,
76 pub pipe_label: Option<Arc<[u8]>>,
77 pub upstream_label: Option<Arc<[u8]>>,
78 pub extra_labels: Labels,
79 pub path_params: BTreeMap<String, String>,
83 #[cfg(feature = "std")]
88 pub cancel: Signal,
89 pub capture: Option<Arc<dyn CaptureContext>>,
93 pub local_upgrade_ticket: Option<u64>,
101 pub peer: Option<PeerInfo>,
107}
108
109impl Default for RequestContext {
110 fn default() -> Self {
111 Self {
112 telemetry: noop_telemetry(),
113 deadline: None,
114 trace_id: None,
115 baggage: None,
116 pipe_label: None,
117 upstream_label: None,
118 extra_labels: Labels::empty(),
119 path_params: BTreeMap::new(),
120 #[cfg(feature = "std")]
121 cancel: never_cancel(),
122 capture: None,
123 local_upgrade_ticket: None,
124 peer: None,
125 }
126 }
127}
128
129impl core::fmt::Debug for RequestContext {
130 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
131 let mut debug = formatter.debug_struct("RequestContext");
132 #[cfg(feature = "std")]
133 debug.field("deadline", &self.deadline);
134 debug
135 .field("trace_id", &self.trace_id)
136 .field("baggage", &self.baggage)
137 .field("pipe_label", &self.pipe_label)
138 .field("upstream_label", &self.upstream_label)
139 .field("extra_labels", &self.extra_labels.entries())
140 .finish_non_exhaustive()
141 }
142}
143
144impl RequestContext {
145 #[must_use]
146 pub fn with_telemetry(mut self, telemetry: TelemetryHandle) -> Self {
147 self.telemetry = telemetry;
148 self
149 }
150
151 #[cfg(feature = "std")]
152 #[must_use]
153 pub fn with_cancel(mut self, cancel: Signal) -> Self {
154 self.cancel = cancel;
155 self
156 }
157
158 #[cfg(feature = "std")]
162 #[must_use]
163 pub fn child_signal(&self) -> Signal {
164 self.cancel.child()
165 }
166
167 #[must_use]
168 pub fn with_pipe_label(mut self, label: impl AsRef<[u8]>) -> Self {
169 self.pipe_label = Some(Arc::from(label.as_ref()));
170 self
171 }
172
173 #[must_use]
174 pub fn with_upstream_label(mut self, label: impl AsRef<[u8]>) -> Self {
175 self.upstream_label = Some(Arc::from(label.as_ref()));
176 self
177 }
178
179 #[must_use]
180 pub fn with_peer(mut self, peer: PeerInfo) -> Self {
181 self.peer = Some(peer);
182 self
183 }
184
185 pub fn adopt_trace_context(&mut self, trace_id: Option<Bytes>, baggage: Option<Bytes>) {
207 if let Some(trace_id) = trace_id {
208 self.trace_id = Some(Arc::from(trace_id.as_ref()));
209 }
210 if let Some(baggage) = baggage {
211 self.baggage = Some(Arc::from(baggage.as_ref()));
212 }
213 }
214
215 #[must_use]
225 pub fn traceparent(&self) -> Option<&[u8]> {
226 self.trace_id.as_deref()
227 }
228
229 pub fn inject_propagation(&self, headers: &mut HeaderList) {
233 if let Some(traceparent) = self.trace_id.as_deref() {
234 headers.insert_if_absent(HeaderName::Traceparent, traceparent);
235 }
236 if let Some(baggage) = self.baggage.as_deref() {
237 headers.insert_if_absent(HeaderName::Baggage, baggage);
238 }
239 }
240
241 #[must_use]
242 pub fn metric_labels(&self, additional: &[(&str, &str)]) -> Labels {
243 let mut pairs: Vec<(String, String)> = self.extra_labels.entries().to_vec();
244 if let Some(pipe) = &self.pipe_label {
245 pairs.push(("pipe".into(), String::from_utf8_lossy(pipe).into_owned()));
246 }
247 if let Some(upstream) = &self.upstream_label {
248 pairs.push((
249 "upstream".into(),
250 String::from_utf8_lossy(upstream).into_owned(),
251 ));
252 }
253 for (name, value) in additional {
254 pairs.push(((*name).to_string(), (*value).to_string()));
255 }
256 let pair_refs: Vec<(&str, &str)> = pairs
257 .iter()
258 .map(|(name, value)| (name.as_str(), value.as_str()))
259 .collect();
260 Labels::from_pairs(&pair_refs)
261 }
262}
263
264#[derive(Debug)]
265pub struct Request<P> {
266 pub method: crate::pipe::method::Method,
270 pub path: Bytes,
273 pub query: HeaderList,
274 pub metadata: HeaderList,
275 pub payload: P,
277 pub stream: Option<RequestStream>,
281 pub context: RequestContext,
282}
283
284impl<P: Clone> Clone for Request<P> {
285 fn clone(&self) -> Self {
292 debug_assert!(
293 self.stream.is_none(),
294 "Request::clone drops a live stream; clone only buffered requests"
295 );
296 Self {
297 method: self.method.clone(),
298 path: self.path.clone(),
299 query: self.query.clone(),
300 metadata: self.metadata.clone(),
301 payload: self.payload.clone(),
302 stream: None,
303 context: self.context.clone(),
304 }
305 }
306}
307
308impl Request<Bytes> {
309 #[must_use]
310 pub fn builder() -> RequestBuilder {
311 RequestBuilder::default()
312 }
313
314 pub async fn body_bytes(self) -> Result<(Self, Bytes), ProximaError> {
318 let mut this = self;
319 if let Some(stream) = this.stream.take() {
320 #[cfg(feature = "std")]
324 let trailers_slot = stream.trailers_slot().cloned();
325 let bytes = stream.collect().await?;
326 #[cfg(feature = "std")]
328 if let Some(slot) = trailers_slot
329 && let Some(trailers) = slot.lock().clone()
330 {
331 for (name, value) in &trailers {
332 this.metadata
333 .insert(Bytes::clone(name), Bytes::clone(value));
334 }
335 }
336 this.payload = Bytes::clone(&bytes);
337 return Ok((this, bytes));
338 }
339 let echo = Bytes::clone(&this.payload);
340 Ok((this, echo))
341 }
342
343 pub fn into_chunk_stream(self) -> ChunkStream {
347 match self.stream {
348 Some(stream) => stream.into_chunk_stream(),
349 None => {
350 let bytes = self.payload;
351 alloc::boxed::Box::pin(futures::stream::once(async move { Ok(bytes) }))
352 }
353 }
354 }
355
356 #[cfg(feature = "part-source")]
366 #[must_use]
367 pub fn from_source(source: &mut impl crate::pipe::part::PartSource) -> Self {
368 use crate::pipe::part::Part;
369
370 let mut method = crate::pipe::method::Method::default();
371 let mut path = Bytes::new();
372 let mut metadata = HeaderList::new();
373 let mut chunks: Vec<u8> = Vec::new();
374
375 while let Some(part) = source.next() {
376 match part {
377 Part::Method(bytes) => method = crate::pipe::method::Method::from_bytes(bytes),
378 Part::Path(bytes) => path = Bytes::copy_from_slice(bytes),
379 Part::Header(name, value) => {
380 metadata.insert(name, value);
381 }
382 Part::Chunk(bytes) => chunks.extend_from_slice(bytes),
383 Part::End => break,
384 }
385 }
386
387 Self {
388 method,
389 path,
390 query: HeaderList::new(),
391 metadata,
392 payload: Bytes::from(chunks),
393 stream: None,
394 context: RequestContext::default(),
395 }
396 }
397}
398
399#[derive(Default)]
400pub struct RequestBuilder {
401 method: Option<crate::pipe::method::Method>,
402 path: Option<Bytes>,
403 query: HeaderList,
404 metadata: HeaderList,
405 payload: Option<Bytes>,
406 stream: Option<RequestStream>,
407 context: RequestContext,
408}
409
410impl core::fmt::Debug for RequestBuilder {
411 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
412 formatter
413 .debug_struct("RequestBuilder")
414 .field("method", &self.method)
415 .field("path", &self.path)
416 .field("query", &self.query)
417 .field("metadata", &self.metadata)
418 .field("has_payload", &self.payload.is_some())
419 .field("context", &self.context)
420 .finish()
421 }
422}
423
424impl RequestBuilder {
425 #[must_use]
426 pub fn method(mut self, method: impl Into<crate::pipe::method::Method>) -> Self {
427 self.method = Some(method.into());
428 self
429 }
430
431 #[must_use]
432 pub fn path(mut self, path: impl crate::pipe::header_list::IntoHeaderBytes) -> Self {
433 self.path = Some(path.into_header_bytes());
434 self
435 }
436
437 #[must_use]
438 pub fn header(
439 mut self,
440 name: impl crate::pipe::header_list::IntoHeaderBytes,
441 value: impl crate::pipe::header_list::IntoHeaderBytes,
442 ) -> Self {
443 self.metadata.insert(name, value);
444 self
445 }
446
447 #[must_use]
448 pub fn query_param(
449 mut self,
450 name: impl crate::pipe::header_list::IntoHeaderBytes,
451 value: impl crate::pipe::header_list::IntoHeaderBytes,
452 ) -> Self {
453 self.query.insert(name, value);
454 self
455 }
456
457 #[must_use]
458 pub fn payload(mut self, payload: impl Into<Bytes>) -> Self {
459 self.payload = Some(payload.into());
460 self
461 }
462
463 #[must_use]
464 pub fn body(mut self, body: impl Into<Bytes>) -> Self {
465 self.payload = Some(body.into());
466 self
467 }
468
469 #[must_use]
471 pub fn stream(mut self, stream: RequestStream) -> Self {
472 self.stream = Some(stream);
473 self
474 }
475
476 #[must_use]
477 pub fn context(mut self, context: RequestContext) -> Self {
478 self.context = context;
479 self
480 }
481
482 #[must_use]
483 pub fn telemetry(mut self, telemetry: TelemetryHandle) -> Self {
484 self.context.telemetry = telemetry;
485 self
486 }
487
488 pub fn build(self) -> Result<Request<Bytes>, ProximaError> {
489 let method = self
490 .method
491 .ok_or_else(|| ProximaError::Config("request method required".into()))?;
492 let path = self
493 .path
494 .ok_or_else(|| ProximaError::Config("request path required".into()))?;
495 Ok(Request {
496 method,
497 path,
498 query: self.query,
499 metadata: self.metadata,
500 payload: self.payload.unwrap_or_default(),
501 stream: self.stream,
502 context: self.context,
503 })
504 }
505}
506
507#[derive(Debug)]
508pub struct Response<P> {
509 pub status: u16,
510 pub metadata: HeaderList,
511 pub payload: P,
513 pub stream: Option<ResponseStream>,
517 #[cfg(feature = "std")]
523 pub upgrade: Option<UpgradeHandler>,
524}
525
526impl<P> Response<P> {
527 #[must_use]
529 pub fn typed(status: u16, payload: P) -> Self {
530 Self {
531 status,
532 metadata: HeaderList::new(),
533 payload,
534 stream: None,
535 #[cfg(feature = "std")]
536 upgrade: None,
537 }
538 }
539
540 #[must_use]
542 pub fn with_stream(mut self, stream: ResponseStream) -> Self {
543 self.stream = Some(stream);
544 self
545 }
546
547 #[cfg(feature = "std")]
551 #[must_use]
552 pub fn with_upgrade(mut self, handler: UpgradeHandler) -> Self {
553 self.upgrade = Some(handler);
554 self
555 }
556
557 #[must_use]
558 pub fn with_header(
559 mut self,
560 name: impl crate::pipe::header_list::IntoHeaderBytes,
561 value: impl crate::pipe::header_list::IntoHeaderBytes,
562 ) -> Self {
563 self.metadata.insert(name, value);
564 self
565 }
566
567 #[must_use]
568 pub fn is_success(&self) -> bool {
569 (200..300).contains(&self.status)
570 }
571
572 #[must_use]
573 pub fn is_no_data(&self) -> bool {
574 self.status == 204
575 }
576}
577
578impl Response<Bytes> {
579 #[must_use]
580 pub fn new(status: u16) -> Self {
581 Self {
582 status,
583 metadata: HeaderList::new(),
584 payload: Bytes::new(),
585 stream: None,
586 #[cfg(feature = "std")]
587 upgrade: None,
588 }
589 }
590
591 #[must_use]
592 pub fn with_payload(mut self, payload: impl Into<Bytes>) -> Self {
593 self.payload = payload.into();
594 self
595 }
596
597 #[must_use]
598 pub fn with_body(mut self, body: impl Into<Bytes>) -> Self {
599 self.payload = body.into();
600 self
601 }
602
603 #[must_use]
605 pub fn streamed(stream: ResponseStream) -> Self {
606 Self::new(200).with_stream(stream)
607 }
608
609 #[must_use]
610 pub fn ok(payload: impl Into<Bytes>) -> Self {
611 Self::new(200).with_payload(payload)
612 }
613
614 #[must_use]
615 pub fn not_found() -> Self {
616 Self::new(404)
617 }
618
619 #[must_use]
620 pub fn no_data() -> Self {
621 Self::new(204)
622 }
623
624 pub fn into_chunk_stream(self) -> ChunkStream {
628 match self.stream {
629 Some(stream) => stream.into_chunk_stream(),
630 None => {
631 let bytes = self.payload;
632 alloc::boxed::Box::pin(futures::stream::once(async move { Ok(bytes) }))
633 }
634 }
635 }
636
637 pub async fn collect_body(self) -> Result<Bytes, ProximaError> {
641 match self.stream {
642 Some(stream) => {
643 stream
644 .collect(
645 #[cfg(feature = "std")]
646 None,
647 )
648 .await
649 }
650 None => Ok(self.payload),
651 }
652 }
653}
654
655#[cfg(all(test, not(loom)))]
660#[allow(clippy::unwrap_used, clippy::expect_used)]
661mod tests {
662 use super::*;
663
664 #[test]
665 fn builder_requires_method() {
666 let outcome = Request::builder().path("/foo").build();
667 assert!(matches!(outcome, Err(ProximaError::Config(_))));
668 }
669
670 #[test]
671 fn builder_requires_path() {
672 let outcome = Request::builder().method("GET").build();
673 assert!(matches!(outcome, Err(ProximaError::Config(_))));
674 }
675
676 #[test]
677 fn builder_assembles_request_with_required_fields() {
678 let request = Request::builder()
679 .method("GET")
680 .path("/users/42")
681 .header("accept", "application/json")
682 .query_param("limit", "10")
683 .build()
684 .expect("builder should succeed");
685 assert_eq!(request.method, "GET");
686 assert_eq!(request.path, "/users/42");
687 assert_eq!(request.metadata.get_str("accept"), Some("application/json"));
688 assert_eq!(request.query.get_str("limit"), Some("10"));
689 }
690
691 #[proxima::test]
692 async fn body_bytes_returns_collected_bytes_and_replaces_body() {
693 let request = Request::builder()
694 .method("POST")
695 .path("/")
696 .body("hello")
697 .build()
698 .expect("builder should succeed");
699 let (request, bytes) = request
700 .body_bytes()
701 .await
702 .expect("body_bytes should succeed");
703 assert_eq!(&bytes[..], b"hello");
704 assert_eq!(
705 &request.payload[..],
706 b"hello",
707 "echo payload should be available for reuse"
708 );
709 }
710
711 #[test]
712 fn response_helpers_set_status() {
713 assert_eq!(Response::ok("body").status, 200);
714 assert_eq!(Response::not_found().status, 404);
715 assert_eq!(Response::no_data().status, 204);
716 }
717
718 #[test]
719 fn is_success_only_2xx() {
720 assert!(Response::new(200).is_success());
721 assert!(Response::new(299).is_success());
722 assert!(!Response::new(199).is_success());
723 assert!(!Response::new(300).is_success());
724 }
725
726 #[test]
727 fn is_no_data_only_204() {
728 assert!(Response::new(204).is_no_data());
729 assert!(!Response::new(200).is_no_data());
730 }
731
732 #[test]
733 fn default_context_is_noop_telemetry_with_empty_labels() {
734 let context = RequestContext::default();
735 assert!(context.deadline.is_none());
736 assert!(context.trace_id.is_none());
737 assert!(context.pipe_label.is_none());
738 assert!(context.extra_labels.is_empty());
739 }
740
741 #[test]
748 fn adopt_trace_context_sets_trace_id_and_baggage() {
749 let mut context = RequestContext::default();
750 context.adopt_trace_context(
751 Some(Bytes::from_static(
752 b"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",
753 )),
754 Some(Bytes::from_static(b"userId=alice")),
755 );
756 assert_eq!(
757 context.trace_id.as_deref(),
758 Some(b"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01".as_slice())
759 );
760 assert_eq!(context.baggage.as_deref(), Some(b"userId=alice".as_slice()));
761 }
762
763 #[test]
764 fn adopt_trace_context_does_not_clobber_baggage_when_absent() {
765 let mut context = RequestContext::default();
766 context.adopt_trace_context(
767 Some(Bytes::from_static(b"trace-a")),
768 Some(Bytes::from_static(b"k=v")),
769 );
770 context.adopt_trace_context(Some(Bytes::from_static(b"trace-b")), None);
771 assert_eq!(context.trace_id.as_deref(), Some(b"trace-b".as_slice()));
772 assert_eq!(
773 context.baggage.as_deref(),
774 Some(b"k=v".as_slice()),
775 "baggage from an earlier adoption survives a re-adoption with no baggage"
776 );
777 }
778
779 #[test]
780 fn inject_propagation_emits_context_traceparent_and_baggage() {
781 let mut context = RequestContext::default();
782 context.adopt_trace_context(
783 Some(Bytes::from_static(
784 b"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",
785 )),
786 Some(Bytes::from_static(b"k=v")),
787 );
788
789 let mut outbound = HeaderList::new();
790 context.inject_propagation(&mut outbound);
791 assert_eq!(
792 outbound.get_str("traceparent"),
793 Some("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01")
794 );
795 assert_eq!(outbound.get_str("baggage"), Some("k=v"));
796 }
797
798 #[test]
799 fn metric_labels_includes_pipe_and_upstream_when_set() {
800 let context = RequestContext::default()
801 .with_pipe_label("echo-cached")
802 .with_upstream_label("origin");
803 let labels = context.metric_labels(&[("status_class", "2xx")]);
804 let entries = labels.entries();
805 assert!(
806 entries
807 .iter()
808 .any(|(name, value)| name == "pipe" && value == "echo-cached")
809 );
810 assert!(
811 entries
812 .iter()
813 .any(|(name, value)| name == "upstream" && value == "origin")
814 );
815 assert!(
816 entries
817 .iter()
818 .any(|(name, value)| name == "status_class" && value == "2xx")
819 );
820 }
821
822 #[test]
823 fn builder_telemetry_overrides_default_noop() {
824 use crate::pipe::telemetry_surface::Telemetry;
825 use core::sync::atomic::{AtomicU64, Ordering};
826
827 struct CountingTelemetry {
828 count: AtomicU64,
829 }
830
831 impl Telemetry for CountingTelemetry {
832 fn counter_inc(&self, _metric: &str, _labels: &Labels, by: u64) {
833 self.count.fetch_add(by, Ordering::Relaxed);
834 }
835 fn gauge_set(&self, _: &str, _: &Labels, _: i64) {}
836 fn histogram_record(&self, _: &str, _: &Labels, _: f64) {}
837 }
838
839 let counting = Arc::new(CountingTelemetry {
840 count: AtomicU64::new(0),
841 });
842 let telemetry: TelemetryHandle = counting.clone();
843 let request = Request::builder()
844 .method("GET")
845 .path("/")
846 .telemetry(telemetry)
847 .build()
848 .expect("builder should succeed");
849 request
850 .context
851 .telemetry
852 .counter_inc("test_counter", &Labels::empty(), 1);
853 assert_eq!(counting.count.load(Ordering::Relaxed), 1);
854 }
855
856 #[test]
860 fn request_and_response_constructible_under_alloc_only() {
861 let request = Request::builder()
862 .method("POST")
863 .path("/items")
864 .header("content-type", "application/json")
865 .build()
866 .expect("request should build");
867 assert_eq!(request.method, "POST");
868 assert_eq!(request.path, "/items");
869
870 let response = Response::new(201)
871 .with_body("created")
872 .with_header("x-id", "42");
873 assert_eq!(response.status, 201);
874 assert_eq!(response.metadata.get_str("x-id"), Some("42"));
875 assert!(response.is_success());
876 }
877
878 #[test]
880 fn request_context_clone_and_peer_label_work_without_std() {
881 let context = RequestContext::default()
882 .with_pipe_label("my-pipe")
883 .with_upstream_label("my-upstream");
884 let cloned = context.clone();
885 assert_eq!(cloned.pipe_label, context.pipe_label);
886 assert_eq!(cloned.upstream_label, context.upstream_label);
887 }
888
889 #[test]
890 fn deadline_field_is_none_by_default() {
891 let context = RequestContext::default();
892 assert!(context.deadline.is_none());
893 }
894
895 #[cfg(feature = "std")]
897 #[test]
898 fn cancel_signal_child_derivation_under_std() {
899 let context = RequestContext::default();
900 let child = context.child_signal();
901 assert!(!child.is_fired());
903 }
904}