kutil_http/cache/
response.rs

1use super::{
2    super::{body::*, headers::*, pieces::*},
3    body::*,
4    configuration::*,
5    hooks::*,
6    weight::*,
7};
8
9use {
10    ::bytes::*,
11    core::any::*,
12    duration_str::*,
13    http::{header::*, response::*, *},
14    http_body::*,
15    kutil_std::error::*,
16    kutil_transcoding::*,
17    std::{io, mem::*, result::Result, sync::*, time::*},
18};
19
20/// Common reference type for [CachedResponse].
21pub type CachedResponseRef = Arc<CachedResponse>;
22
23//
24// CachedResponse
25//
26
27/// Cached HTTP response.
28///
29/// Caching the body is handled by [CachedBody].
30#[derive(Clone, Debug)]
31pub struct CachedResponse {
32    /// Response parts.
33    pub parts: Parts,
34
35    /// Response body.
36    pub body: CachedBody,
37
38    /// Optional duration.
39    pub duration: Option<Duration>,
40}
41
42impl CachedResponse {
43    /// Constructor.
44    ///
45    /// Reads the response body and stores it as [Bytes].
46    ///
47    /// If `known_body_size` is not [None](Option::None) then that's the size we expect. Otherwise
48    /// we'll try to read to `max_body_size` and will expect at least `min_body_size`.
49    ///
50    /// In either case we will return an error if the body wasn't completely read (we won't cache
51    /// incomplete bodies!), together with [ResponsePieces], which can be used by the caller to
52    /// reconstruct the original response.
53    ///
54    /// `preferred_encoding` is the encoding in which we *want* to store the body. If the response's
55    /// encoding is different from what we want then it will be reencoded, unless the `XX-Encode`
56    /// header is "false", in which case it's as if `preferred_encoding` were
57    /// [Identity](Encoding::Identity).
58    ///
59    /// If an [Identity](Encoding::Identity) is created during this reencoding then it will also be
60    /// stored if `keep_identity_encoding` is true.
61    ///
62    /// If the response doesn't already have a `Last-Modified` header, we will set it to the
63    /// current time.
64    pub async fn new_for<BodyT>(
65        uri: &Uri,
66        response: Response<BodyT>,
67        declared_body_size: Option<usize>,
68        mut preferred_encoding: Encoding,
69        skip_encoding: bool,
70        caching_configuration: &CachingConfiguration,
71        encoding_configuration: &EncodingConfiguration,
72    ) -> Result<Self, ErrorWithResponsePieces<ReadBodyError, BodyT>>
73    where
74        BodyT: Body + Unpin,
75        BodyT::Error: Into<CapturedError>,
76    {
77        let (mut parts, body) = response.into_parts();
78
79        let bytes = match body
80            .read_into_bytes_or_pieces(
81                declared_body_size,
82                caching_configuration.min_body_size,
83                caching_configuration.max_body_size,
84            )
85            .await
86        {
87            Ok((bytes, _trailers)) => bytes,
88            Err(error) => {
89                return Err(ErrorWithResponsePieces::new_from_body(error, parts));
90            }
91        };
92
93        if preferred_encoding != Encoding::Identity {
94            if !parts.headers.xx_encode(encoding_configuration.encodable_by_default) {
95                tracing::debug!("not encoding to {} ({}=false)", preferred_encoding, XX_ENCODE);
96                preferred_encoding = Encoding::Identity;
97            } else if bytes.len() < encoding_configuration.min_body_size {
98                tracing::debug!("not encoding to {} (too small)", preferred_encoding);
99                preferred_encoding = Encoding::Identity;
100            }
101        }
102
103        let body = CachedBody::new_with(
104            bytes,
105            parts.headers.content_encoding().into(),
106            preferred_encoding,
107            encoding_configuration,
108        )
109        .await
110        // This is not *exactly* a ReadBodyError, but rather an encoding error for the read body
111        .map_err(|error| ErrorWithResponsePieces::from(ReadBodyError::from(error)))?;
112
113        // Extract `XX-Cache-Duration` or call hook
114        let duration = match parts.headers.xx_cache_duration() {
115            Some(duration) => Some(duration),
116            None => match &caching_configuration.cache_duration {
117                Some(cache_duration) => cache_duration(CacheDurationHookContext::new(uri, &parts.headers)),
118                None => None,
119            },
120        };
121
122        if let Some(duration) = duration {
123            tracing::debug!("duration: {}", duration.human_format());
124        }
125
126        // Make sure we have a `Last-Modified`
127        if !parts.headers.contains_key(LAST_MODIFIED) {
128            parts.headers.set_into_header_value(LAST_MODIFIED, now());
129        }
130
131        parts.headers.remove(XX_CACHE);
132        parts.headers.remove(XX_CACHE_DURATION);
133        parts.headers.remove(CONTENT_ENCODING);
134        parts.headers.remove(CONTENT_LENGTH);
135        parts.headers.remove(CONTENT_DIGEST);
136
137        // Note that we are keeping the `XX-Encode` header in the cache
138        // (but will remove it in `to_response`)
139
140        if skip_encoding {
141            parts.headers.set_bool_value(XX_ENCODE, true);
142        }
143
144        // TODO: can we support ranges? if so, we should not remove this header
145        // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Ranges
146        parts.headers.remove(ACCEPT_RANGES);
147
148        Ok(Self { parts, body, duration })
149    }
150
151    /// Clone with new body.
152    pub fn clone_with_body(&self, body: CachedBody) -> Self {
153        Self { parts: self.parts.clone(), body, duration: self.duration.clone() }
154    }
155
156    /// Headers.
157    pub fn headers(&self) -> &HeaderMap {
158        &self.parts.headers
159    }
160
161    /// Create a [Response].
162    ///
163    /// If we don't have the specified encoding then we will reencode from another encoding,
164    /// storing the result so that we won't have to encode it again.
165    ///
166    /// If an [Identity](Encoding::Identity) is created during this reencoding then it will also be
167    /// stored if `keep_identity_encoding` is true.
168    ///
169    /// If the stored `XX-Encode` header is "false" then will ignore the specified encoding and
170    /// return an [Identity](Encoding::Identity) response.
171    ///
172    /// Returns a modified clone if reencoding caused a new encoding to be stored. Note that
173    /// cloning should be cheap due to our use of [Bytes] in the body.
174    pub async fn to_response<BodyT>(
175        &self,
176        mut encoding: &Encoding,
177        configuration: &EncodingConfiguration,
178    ) -> io::Result<(Response<BodyT>, Option<Self>)>
179    where
180        BodyT: Body + From<Bytes>,
181    {
182        if (*encoding != Encoding::Identity) && !self.headers().xx_encode(configuration.encodable_by_default) {
183            tracing::debug!("not encoding to {} ({}=false)", encoding, XX_ENCODE);
184            encoding = &Encoding::Identity;
185        }
186
187        let (bytes, modified) = self.body.get(encoding, configuration).await?;
188
189        let mut parts = self.parts.clone();
190
191        parts.headers.remove(XX_ENCODE);
192
193        if *encoding != Encoding::Identity {
194            // No need to specify Identity as it's the default
195            parts.headers.set_into_header_value(CONTENT_ENCODING, encoding.clone());
196        }
197
198        parts.headers.set_value(CONTENT_LENGTH, bytes.len());
199
200        Ok((Response::from_parts(parts, bytes.into()), modified.map(|body| self.clone_with_body(body))))
201    }
202}
203
204impl CacheWeight for CachedResponse {
205    fn cache_weight(&self) -> usize {
206        const SELF_SIZE: usize = size_of::<CachedResponse>();
207        const HEADER_MAP_ENTRY_SIZE: usize = size_of::<HeaderName>() + size_of::<HeaderValue>();
208        const EXTENSION_ENTRY_SIZE: usize = size_of::<TypeId>();
209
210        let mut size = SELF_SIZE;
211
212        let parts = &self.parts;
213        for (name, value) in &parts.headers {
214            size += HEADER_MAP_ENTRY_SIZE + name.as_str().len() + value.len()
215        }
216        size += parts.extensions.len() * EXTENSION_ENTRY_SIZE;
217
218        size += self.body.cache_weight();
219
220        size
221    }
222}