Skip to main content

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}