hitbox_http/request.rs
1use hitbox::{
2 CacheablePolicyData, RequestCachePolicy,
3 predicate::{Predicate, PredicateResult},
4 {CachePolicy, CacheableRequest, Extractor},
5};
6use http::{Request, request::Parts};
7use hyper::body::Body as HttpBody;
8
9use crate::CacheableSubject;
10use crate::body::BufferedBody;
11use crate::predicates::header::HasHeaders;
12use crate::predicates::version::HasVersion;
13
14/// Wraps an HTTP request for cache policy evaluation.
15///
16/// This type holds the request metadata ([`Parts`]) and a [`BufferedBody`] that
17/// allows predicates and extractors to inspect the request without fully consuming
18/// the body stream.
19///
20/// # Type Parameters
21///
22/// * `ReqBody` - The HTTP request body type. Must implement [`hyper::body::Body`]
23/// with `Send` bounds. Common concrete types:
24/// - [`Empty<Bytes>`](http_body_util::Empty) - No body (GET requests)
25/// - [`Full<Bytes>`](http_body_util::Full) - Complete body in memory
26/// - `BoxBody<Bytes, E>` - Type-erased body for dynamic dispatch
27///
28/// # Examples
29///
30/// ```
31/// use bytes::Bytes;
32/// use http::Request;
33/// use http_body_util::Empty;
34/// use hitbox_http::{BufferedBody, CacheableHttpRequest};
35///
36/// let request = Request::builder()
37/// .method("GET")
38/// .uri("/users/42")
39/// .header("Authorization", "Bearer token")
40/// .body(BufferedBody::Passthrough(Empty::<Bytes>::new()))
41/// .unwrap();
42///
43/// let cacheable = CacheableHttpRequest::from_request(request);
44/// ```
45///
46/// # Extracting Cache Keys
47///
48/// Use with extractors to generate cache key parts:
49///
50/// ```no_run
51/// use hitbox::Extractor;
52/// use hitbox_http::CacheableHttpRequest;
53/// use hitbox_http::extractors::{Method, path::PathExtractor};
54///
55/// # use bytes::Bytes;
56/// # use http_body_util::Empty;
57/// # use hitbox_http::extractors::{NeutralExtractor, Path};
58/// async fn example(cacheable: CacheableHttpRequest<Empty<Bytes>>) {
59/// let extractor = Method::new().path("/users/{user_id}");
60/// # let _: &Path<Method<NeutralExtractor<Empty<Bytes>>>> = &extractor;
61/// let key_parts = extractor.get(cacheable).await;
62/// }
63/// ```
64#[derive(Debug)]
65pub struct CacheableHttpRequest<ReqBody>
66where
67 ReqBody: HttpBody,
68{
69 parts: Parts,
70 body: BufferedBody<ReqBody>,
71}
72
73impl<ReqBody> CacheableHttpRequest<ReqBody>
74where
75 ReqBody: HttpBody,
76{
77 /// Creates a cacheable request from an HTTP request with a buffered body.
78 ///
79 /// The request body must already be wrapped in a [`BufferedBody`]. Use
80 /// [`BufferedBody::Passthrough`] for requests that haven't been inspected yet.
81 pub fn from_request(request: Request<BufferedBody<ReqBody>>) -> Self {
82 let (parts, body) = request.into_parts();
83 Self { parts, body }
84 }
85
86 /// Converts back into a standard HTTP request.
87 ///
88 /// Use this after cache policy evaluation to continue processing the request.
89 pub fn into_request(self) -> Request<BufferedBody<ReqBody>> {
90 Request::from_parts(self.parts, self.body)
91 }
92
93 /// Returns a reference to the request metadata.
94 ///
95 /// The [`Parts`] contain the method, URI, version, headers, and extensions.
96 pub fn parts(&self) -> &Parts {
97 &self.parts
98 }
99
100 /// Decomposes into metadata and body.
101 ///
102 /// This is equivalent to [`CacheableSubject::into_parts`].
103 pub fn into_parts(self) -> (Parts, BufferedBody<ReqBody>) {
104 (self.parts, self.body)
105 }
106}
107
108impl<ReqBody> CacheableSubject for CacheableHttpRequest<ReqBody>
109where
110 ReqBody: HttpBody,
111{
112 type Body = ReqBody;
113 type Parts = Parts;
114
115 fn into_parts(self) -> (Self::Parts, BufferedBody<Self::Body>) {
116 (self.parts, self.body)
117 }
118
119 fn from_parts(parts: Self::Parts, body: BufferedBody<Self::Body>) -> Self {
120 Self { parts, body }
121 }
122}
123
124impl<ReqBody> HasHeaders for CacheableHttpRequest<ReqBody>
125where
126 ReqBody: HttpBody,
127{
128 fn headers(&self) -> &http::HeaderMap {
129 &self.parts.headers
130 }
131}
132
133impl<ReqBody> HasVersion for CacheableHttpRequest<ReqBody>
134where
135 ReqBody: HttpBody,
136{
137 fn http_version(&self) -> http::Version {
138 self.parts.version
139 }
140}
141
142impl<ReqBody> CacheableRequest for CacheableHttpRequest<ReqBody>
143where
144 ReqBody: HttpBody + Send + 'static,
145 ReqBody::Error: Send,
146{
147 async fn cache_policy<P, E>(self, predicates: P, extractors: E) -> RequestCachePolicy<Self>
148 where
149 P: Predicate<Subject = Self> + Send + Sync,
150 E: Extractor<Subject = Self> + Send + Sync,
151 {
152 let (request, key) = extractors.get(self).await.into_cache_key();
153
154 match predicates.check(request).await {
155 PredicateResult::Cacheable(request) => {
156 CachePolicy::Cacheable(CacheablePolicyData { key, request })
157 }
158 PredicateResult::NonCacheable(request) => CachePolicy::NonCacheable(request),
159 }
160 }
161}