Skip to main content

tower_http_cache_plus/
layer.rs

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