kutil_http/tower/caching/
layer.rs

1use super::{
2    super::super::{
3        cache::{middleware::*, *},
4        headers::*,
5    },
6    service::*,
7};
8
9use {
10    std::{marker::*, sync::*, time::*},
11    tower::*,
12};
13
14//
15// CachingLayer
16//
17
18/// HTTP response caching layer with integrated encoding (compression).
19///
20/// Though you can rely on an external caching solution instead (e.g. a reverse proxy), there are
21/// good reasons to integrate the cache directly into your application. For one, direct access
22/// allows for an in-process in-memory cache, which is optimal for at least the first caching tier.
23///
24/// When both caching and encoding are enabled it will avoid unnecessary reencoding by storing
25/// encoded versions in the cache. A cache hit will thus be able to handle HTTP content negotiation
26/// (the `Accept-Encoding` header) instead of the upstream. This is an important compute
27/// optimization that is impossible to achieve if encoding and caching are implemented as
28/// independent layers. Far too many web servers ignore this optimization and waste compute
29/// resources reencoding data that has not changed.
30///
31/// This layer also participates in client-side caching (conditional HTTP). A cache hit will
32/// respect the client's `If-None-Match` and `If-Modified-Since` headers and return a 304 (Not
33/// Modified) when appropriate, saving bandwidth as well as compute resources. If you don't set a
34/// `Last-Modified` header yourself then this layer will default to the instant in which the cache
35/// entry was created.
36///
37/// For encoding we support the web's common compression formats: Brotli, Deflate, GZip, and
38/// Zstandard. We select the best encoding according to our and the client's preferences (HTTP
39/// content negotiation).
40///
41/// The cache and cache key implementations are provided as generic type parameters. The
42/// [CommonCacheKey] implementation should suffice for common use cases.
43///
44/// Access to the cache is `async`, though note that concurrent performance will depend on the
45/// actual cache implementation, the HTTP server, and of course your async runtime.
46///
47/// Please check out the
48/// [included examples](https://github.com/tliron/rust-kutil/tree/main/crates/http/examples)!
49///
50/// Requirements
51/// ============
52///
53/// The response body type *and* its data type must both implement
54/// [From]\<[Bytes](::bytes::Bytes)\>. (This is supported by
55/// [axum](https://github.com/tokio-rs/axum).) Note that even though
56/// [Tokio](https://github.com/tokio-rs/tokio) I/O types are used internally, this layer does *not*
57/// require a specific async runtime.
58///
59/// Usage notes
60/// ===========
61///
62/// 1. By default this layer is "opt-out" for caching and encoding. You can "punch through" this
63///    behavior via custom response headers (which will be removed before sending the response
64///    downstream):
65///
66///    * Set `XX-Cache` to "false" to skip caching.
67///    * Set `XX-Encode` to "false" to skip encoding.
68///
69///    However, you can also configure for "opt-in", *requiring* these headers to be set to "true"
70///    in order to enable the features. See [cacheable_by_default](Self::cacheable_by_default) and
71///    [encodable_by_default](Self::encodable_by_default).
72///
73/// 2. Alternatively, you can provide [cacheable_by_request](Self::cacheable_by_request),
74///    [cacheable_by_response](Self::cacheable_by_response),
75///    [encodable_by_request](Self::encodable_by_request),
76///    and/or [encodable_by_response](Self::encodable_by_response) hooks to control these features.
77///    (If not provided they are assumed to return true.) The response hooks can be workarounds for
78///    when you can't add custom headers upstream.
79///
80/// 3. You can explicitly set the cache duration for a response via a `XX-Cache-Duration` header.
81///    Its string value is parsed using [duration-str](https://github.com/baoyachi/duration-str).
82///    You can also provide a [cache_duration](Self::cache_duration) hook (the
83///    `XX-Cache-Duration` header will override it). The actual effect of the duration depends on
84///    the cache implementation.
85///
86///    ([Here](https://docs.rs/moka/latest/moka/policy/trait.Expiry.html#method.expire_after_create)
87///    is the logic used for the Moka implementation.)
88///
89/// 4. Though this layer transparently handles HTTP content negotiation for `Accept-Encoding`, for
90///    which the underlying content is the same, it cannot do so for `Accept` and
91///    `Accept-Language`, for which content can differ. We do, however, provide a solution for
92///    situations in which negotiation can be handled *without* the upstream response: the
93///    [cache_key](Self::cache_key) hook. Here you can handle negotiation yourself and update the
94///    cache key accordingly, so that different content will be cached separately. [CommonCacheKey]
95///    reserves fields for media type and languages, just for this purpose.
96///
97///    If this impossible or too cumbersome, the alternative to content negotiation is to make
98///    content selection the client's responsibility by including the content type in the URL, in
99///    the path itself or as a query parameter. Web browsers often rely on JavaScript to automate
100///    this for users by switching to the appropriate URL, for example adding "/en" to the path to
101///    select English.
102///
103/// General advice
104/// ==============
105///
106/// 1. Compressing already-compressed content is almost always a waste of compute for both the
107///    server and the client. For this reason it's a good idea to explicitly skip the encoding of
108///    [MIME types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types/Common_types)
109///    that are known to be already-compressed, such as those for audio, video, and images. You can
110///    do this via the [encodable_by_response](Self::encodable_by_response) hook mentioned above.
111///    (See the example.)
112///
113/// 2. We advise setting the `Content-Length` header on your responses whenever possible as it
114///    allows this layer to check for cacheability without having to read the body, and it's
115///    generally a good practice that helps many HTTP components to run optimally. That said, this
116///    layer will optimize as much as it can even when `Content-Length` is not available, reading
117///    only as many bytes as necessary to determine if the response is cacheable and then "pushing
118///    back" those bytes (zero-copy) if it decides to skip the cache and send the response
119///    downstream.
120///
121/// 3. Make use of client-side caching by setting the `Last-Modified` and/or `ETag` headers on your
122///    responses. They are of course great without server-side caching, but this layer will respect
123///    them even for cached entries, returning 304 (Not Modified) when appropriate.
124///
125/// 4. This caching layer does *not* own the cache, meaning that you can can insert or invalidate
126///    cache entries according to application events other than user requests. Example scenarios:
127///
128///    1. Inserting cache entries manually can be critical for avoiding "cold cache" performance
129///       degradation (as well as outright failure) for busy, resource-heavy servers. You might
130///       want to initialize your cache with popular entries before opening your server to
131///       requests. If your cache is distributed it might also mean syncing the cache first.
132///
133///    2. Invalidating cache entries manually can be critical for ensuring that clients don't
134///       see out-of-date data, especially when your cache durations are long. For example, when
135///       certain data is deleted from your database you can make sure to invalidate all cache
136///       entries that depend on that data. To simplify this, you can the data IDs to your cache
137///       keys. When invalidating, you can then enumerate all existing keys that contain the
138///       relevant ID. [CommonCacheKey] reserves an `extensions` fields just for this purpose.
139///
140/// Request handling
141/// ================
142///
143/// Here we'll go over the complete processing flow in detail:
144///
145/// 1. A request arrives. Check if it is cacheable (for now). Reasons it won't be cacheable:
146///
147///    * Caching is disabled for this layer
148///    * The request is non-idempotent (e.g. POST)
149///    * If we pass the checks above then we give the
150///      [cacheable_by_request](Self::cacheable_by_request) hook a chance to skip caching.
151///      If it returns false then we are non-cacheable.
152///
153///    If the response is non-cacheable then go to "Non-cached request handling" below.
154///
155/// 2. Check if we have a cached response.
156///
157/// 3. If we do, then:
158///
159///    1. Select the best encoding according to our configured preferences and the priorities
160///       specified in the request's `Accept-Encoding`. If the cached response has `XX-Encode`
161///       header as "false" then use Identity encoding.
162///
163///    2. If we have that encoding in the cache then:
164///
165///       1. If the client sent `If-Modified-Since` then compare with our cached `Last-Modified`,
166///          and if not modified then send a 304 (Not Modified) status (conditional HTTP). END.
167///
168///       2. Otherwise create a response from the cache entry and send it. Note that we know its
169///          size so we set `Content-Length` accordingly. END.
170///
171///    3. Otherwise, if we don't have the encoding in the cache then check to see if the cache
172///       entry has `XX-Encode` entry as "false". If so, we will choose Identity encoding and go up
173///       to step 3.2.2.
174///
175///    4. Find the best starting point from the encodings we already have. We select them in order
176///       from cheapest to decode (Identity) to the most expensive.
177///
178///    5. If the starting point encoding is *not* Identity then we must first decode it. If
179///       `keep_identity_encoding` is true then we will store the decoded data in the cache so that
180///       we can skip this step in the future (the trade-off is taking up more room in the cache).
181///
182///    6. Encode the body and store it in the cache.
183///
184///    7. Go up to step 3.2.2.
185///
186/// 4. If we don't have a cached response:
187///
188///    1. Get the upstream response and check if it is cacheable. Reasons it won't be cacheable:
189///
190///       * Its status code is not "success" (200 to 299)
191///       * Its `XX-Cache` header is "false"
192///       * It has a `Content-Range` header (we don't cache partial responses)
193///       * It has a `Content-Length` header that is lower than our configured minimum or higher
194///         than our configured maximum
195///       * If we pass all the checks above then we give the
196///         [cacheable_by_response](Self::cacheable_by_response) hook one last chance to skip
197///         caching. If it returns false then we are non-cacheable.
198///
199///       If the upstream response is non-cacheable then go to "Non-cached request handling" below.
200///
201///    2. Otherwise select the best encoding according to our configured preferences and the
202///       priorities specified in the request's `Accept-Encoding`. If the upstream response has
203///       `XX-Encode` header as "false" or has `Content-Length` smaller than our configured
204///       minimum, then use Identity encoding.
205///
206///    3. If the selected encoding is not Identity then we give the
207///       [encodable_by_response](Self::encodable_by_response) hook one last chance to skip
208///       encoding. If it returns false we set the encoding to Identity and add the `XX-Encode`
209///       header as "true" for use by step 3.1 above.
210///
211///    4. Read the upstream response body into a buffer. If there is no `Content-Length` header
212///       then make sure to read no more than our configured maximum size.
213///
214///    5. If there's still more data left or the data that was read is less than our configured
215///       minimum size then it means the upstream response is non-cacheable, so:
216///
217///       1. Push the data that we read back into the front of the upstream response body.
218///
219///       2. Go to "Non-cached request handling" step 4 below.
220///
221///    6. Otherwise store the read bytes in the cache, encoding them if necessary. We know the
222///       size, so we can check if it's smaller than the configured minimum for encoding, in
223///       which case we use Identity encoding. We also make sure to set the cached `Last-Modified`
224///       header to the current time if the header wasn't already set. Go up to step 3.2.
225///
226///       Note that upstream response trailers are discarded and *not* stored in the cache. (We
227///       make the assumption that trailers are only relevant to "real" responses.)
228///
229/// ### Non-cached request handling
230///
231/// 1. If the upstream response has `XX-Encode` header as "false" or has `Content-Length` smaller
232///    than our configured minimum, then pass it through as is. THE END.
233///
234///    Note that without `Content-Length` there is no way for us to check against the minimum and
235///    so we must continue.
236///
237/// 2. Select the best encoding according to our configured preferences and the priorities
238///    specified in the request's `Accept-Encoding`.
239///
240/// 3. If the selected encoding is not Identity then we give the
241///    [encodable_by_request](Self::encodable_by_request) and
242///    [encodable_by_response](Self::encodable_by_response) hooks one last chance to skip encoding.
243///    If either returns false we set the encoding to Identity.
244///
245/// 4. If the upstream response is already in the selected encoding then pass it through. END.
246///
247/// 5. Otherwise, if the upstream response is Identity, then wrap it in an encoder and send it
248///    downstream. Note that we do not know the encoded size in advance so we make sure there is no
249///    `Content-Length` header. END.
250///
251/// 6. However, if the upstream response is *not* Identity, then just pass it through as is. END.
252///
253///    Note that this is technically wrong and in fact there is no guarantee here that the client
254///    would support the upstream response's encoding. However, we implement it this way because:
255///
256///    1) This is likely a rare case. If you are using this middleware then you probably don't have
257///       already-encoded data coming from previous layers.
258///
259///    2) If you do have already-encoded data, it is reasonable to expect that the encoding was
260///       selected according to the request's `Accept-Encoding`.
261///
262///    3) It's quite a waste of compute to decode and then reencode, which goes against the goals
263///       of this middleware. (We do emit a warning in the logs.)
264pub struct CachingLayer<RequestBodyT, CacheT, CacheKeyT = CommonCacheKey>
265where
266    CacheT: Cache<CacheKeyT>,
267    CacheKeyT: CacheKey,
268{
269    caching: MiddlewareCachingConfiguration<RequestBodyT, CacheT, CacheKeyT>,
270    encoding: MiddlewareEncodingConfiguration,
271}
272
273impl<RequestBodyT, CacheT, CacheKeyT> CachingLayer<RequestBodyT, CacheT, CacheKeyT>
274where
275    CacheT: Cache<CacheKeyT>,
276    CacheKeyT: CacheKey,
277{
278    /// Constructor.
279    pub fn new() -> Self {
280        Self {
281            caching: MiddlewareCachingConfiguration::default(),
282            encoding: MiddlewareEncodingConfiguration::default(),
283        }
284    }
285
286    /// Enable cache.
287    ///
288    /// Not enabled by default.
289    pub fn cache(mut self, cache: CacheT) -> Self {
290        self.caching.cache = Some(cache);
291        self
292    }
293
294    /// Minimum size in bytes of response bodies to cache.
295    ///
296    /// The default is 0.
297    pub fn min_cacheable_body_size(mut self, min_cacheable_body_size: usize) -> Self {
298        self.caching.inner.min_body_size = min_cacheable_body_size;
299        self
300    }
301
302    /// Maximum size in bytes of response bodies to cache.
303    ///
304    /// The default is 1 MiB.
305    pub fn max_cacheable_body_size(mut self, max_cacheable_body_size: usize) -> Self {
306        self.caching.inner.max_body_size = max_cacheable_body_size;
307        self
308    }
309
310    /// If a response does not specify the `XX-Cache` response header then this we will assume its
311    /// value is this.
312    ///
313    /// The default is true.
314    pub fn cacheable_by_default(mut self, cacheable_by_default: bool) -> Self {
315        self.caching.inner.cacheable_by_default = cacheable_by_default;
316        self
317    }
318
319    /// Provide a hook to test whether a request is cacheable.
320    ///
321    /// Will only be called after all internal conditions are met, giving you one last chance to
322    /// prevent caching.
323    ///
324    /// Note that the headers are *request* headers. This hook is called before we have the
325    /// upstream response.
326    ///
327    /// [None](Option::None) by default.
328    pub fn cacheable_by_request(
329        mut self,
330        cacheable_by_request: impl Fn(CacheableHookContext) -> bool + 'static + Send + Sync,
331    ) -> Self {
332        self.caching.cacheable_by_request = Some(Arc::new(Box::new(cacheable_by_request)));
333        self
334    }
335
336    /// Provide a hook to test whether an upstream response is cacheable.
337    ///
338    /// Will only be called after all internal conditions are met, giving you one last chance to
339    /// prevent caching.
340    ///
341    /// Note that the headers are *response* headers. This hook is called *after* we get the
342    /// upstream response but *before* we read its body.
343    ///
344    /// [None](Option::None) by default.
345    pub fn cacheable_by_response(
346        mut self,
347        cacheable_by_response: impl Fn(CacheableHookContext) -> bool + 'static + Send + Sync,
348    ) -> Self {
349        self.caching.cacheable_by_response = Some(Arc::new(Box::new(cacheable_by_response)));
350        self
351    }
352
353    /// [None](Option::None) by default.
354    pub fn cache_key(
355        mut self,
356        cache_key: impl Fn(CacheKeyHookContext<CacheKeyT, RequestBodyT>) + 'static + Send + Sync,
357    ) -> Self {
358        self.caching.cache_key = Some(Arc::new(Box::new(cache_key)));
359        self
360    }
361
362    /// Provide a hook to get a response's cache duration.
363    ///
364    /// Will only be called if an `XX-Cache-Duration` response header is *not* provided. In other
365    /// words, `XX-Cache-Duration` will always override this value.
366    ///
367    /// Note that the headers are *response* headers.
368    ///
369    /// [None](Option::None) by default.
370    pub fn cache_duration(
371        mut self,
372        cache_duration: impl Fn(CacheDurationHookContext) -> Option<Duration> + 'static + Send + Sync,
373    ) -> Self {
374        self.caching.inner.cache_duration = Some(Arc::new(Box::new(cache_duration)));
375        self
376    }
377
378    /// Enable encodings in order from most preferred to least.
379    ///
380    /// Will be negotiated with the client's preferences (in its `Accept-Encoding` header) to
381    /// select the best.
382    ///
383    /// There is no need to specify [Identity](kutil_transcoding::Encoding::Identity) as it is
384    /// always enabled.
385    ///
386    /// The default is [ENCODINGS_BY_PREFERENCE].
387    pub fn enable_encodings(mut self, enabled_encodings_by_preference: Vec<EncodingHeaderValue>) -> Self {
388        self.encoding.enabled_encodings_by_preference = Some(enabled_encodings_by_preference);
389        self
390    }
391
392    /// Disables encoding.
393    ///
394    /// The default is [ENCODINGS_BY_PREFERENCE].
395    pub fn disable_encoding(mut self) -> Self {
396        self.encoding.enabled_encodings_by_preference = None;
397        self
398    }
399
400    /// Minimum size in bytes of response bodies to encode.
401    ///
402    /// Note that non-cached responses without `Content-Length` cannot be checked against this
403    /// value.
404    ///
405    /// The default is 0.
406    pub fn min_encodable_body_size(mut self, min_encodable_body_size: usize) -> Self {
407        self.encoding.inner.min_body_size = min_encodable_body_size;
408        self
409    }
410
411    /// If a response does not specify the `XX-Encode` response header then this we will assume its
412    /// value is this.
413    ///
414    /// The default is true.
415    pub fn encodable_by_default(mut self, encodable_by_default: bool) -> Self {
416        self.encoding.inner.encodable_by_default = encodable_by_default;
417        self
418    }
419
420    /// Provide a hook to test whether a request is encodable.
421    ///
422    /// Will only be called after all internal conditions are met, giving you one last chance to
423    /// prevent encoding.
424    ///
425    /// Note that the headers are *request* headers. This hook is called before we have the
426    /// upstream response.
427    ///
428    /// [None](Option::None) by default.
429    pub fn encodable_by_request(
430        mut self,
431        encodable_by_request: impl Fn(EncodableHookContext) -> bool + 'static + Send + Sync,
432    ) -> Self {
433        self.encoding.encodable_by_request = Some(Arc::new(Box::new(encodable_by_request)));
434        self
435    }
436
437    /// Provide a hook to test whether a response is encodable.
438    ///
439    /// Will only be called after all internal conditions are met, giving you one last chance to
440    /// prevent encoding.
441    ///
442    /// Note that the headers are *response* headers. This hook is called *after* we get the
443    /// upstream response but *before* we read its body.
444    ///
445    /// [None](Option::None) by default.
446    pub fn encodable_by_response(
447        mut self,
448        encodable_by_response: impl Fn(EncodableHookContext) -> bool + 'static + Send + Sync,
449    ) -> Self {
450        self.encoding.encodable_by_response = Some(Arc::new(Box::new(encodable_by_response)));
451        self
452    }
453
454    /// Whether to keep an [Identity](kutil_transcoding::Encoding::Identity) in the cache if it is
455    /// created during reencoding.
456    ///
457    /// Keeping it optimizes for compute with the trade-off of taking up more room in the cache.
458    ///
459    /// The default is true.
460    pub fn keep_identity_encoding(mut self, keep_identity_encoding: bool) -> Self {
461        self.encoding.inner.keep_identity_encoding = keep_identity_encoding;
462        self
463    }
464}
465
466impl<RequestBodyT, CacheT, CacheKeyT> Clone for CachingLayer<RequestBodyT, CacheT, CacheKeyT>
467where
468    CacheT: Cache<CacheKeyT>,
469    CacheKeyT: CacheKey,
470{
471    fn clone(&self) -> Self {
472        Self { caching: self.caching.clone(), encoding: self.encoding.clone() }
473    }
474}
475
476impl<InnerServiceT, RequestBodyT, CacheT, CacheKeyT> Layer<InnerServiceT>
477    for CachingLayer<RequestBodyT, CacheT, CacheKeyT>
478where
479    CacheT: Cache<CacheKeyT>,
480    CacheKeyT: CacheKey,
481{
482    type Service = CachingService<InnerServiceT, RequestBodyT, CacheT, CacheKeyT>;
483
484    fn layer(&self, inner_service: InnerServiceT) -> Self::Service {
485        CachingService::new(inner_service, self.caching.clone(), self.encoding.clone())
486    }
487}