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}