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}