├── .gitignore ├── fresh.jpg ├── .rustfmt.toml ├── .github └── workflows │ └── cargo.yml ├── LICENSE ├── Cargo.toml ├── tests ├── request.rs ├── update.rs ├── satisfy.rs ├── vary.rs ├── okhttp.rs ├── revalidate.rs ├── response.rs ├── responsetest.rs └── tests.rs ├── README.md ├── examples └── interactive.rs └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | Cargo.lock 3 | .DS_Store 4 | 5 | -------------------------------------------------------------------------------- /fresh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kornelski/rusty-http-cache-semantics/HEAD/fresh.jpg -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | .please do not use rustfmt, it destroys well-formatted code 2 | 3 | disable_all_formatting = true 4 | ignore = ["/"] 5 | -------------------------------------------------------------------------------- /.github/workflows/cargo.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: 7 | # Branches from forks have the form 'user:branch-name' so we only run 8 | # this job on pull_request events for branches that look like fork 9 | # branches. Without this we would end up running this job twice for non 10 | # forked PRs, once for the push and then once for opening the PR. 11 | - '**:**' 12 | 13 | jobs: 14 | test: 15 | name: Test 16 | strategy: 17 | matrix: 18 | rust: 19 | - stable 20 | - nightly 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v2 24 | - uses: actions-rs/toolchain@v1 25 | with: 26 | profile: minimal 27 | toolchain: ${{ matrix.rust }} 28 | override: true 29 | - uses: actions-rs/cargo@v1 30 | with: 31 | command: test 32 | args: --all-features 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016-2020 Kornel Lesiński 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "http-cache-semantics" 3 | version = "2.1.0" 4 | description = "RFC 7234. Parses HTTP headers to correctly compute cacheability of responses, even in complex cases" 5 | homepage = "https://lib.rs/http-cache-semantics" 6 | repository = "https://github.com/kornelski/rusty-http-cache-semantics" 7 | documentation = "https://docs.rs/http-cache-semantics" 8 | license = "BSD-2-Clause" 9 | authors = ["Kornel ", "Luna Graysen ", "Douglas Greenshields ", "Kat Marchán "] 10 | edition = "2021" 11 | categories = ["caching", "web-programming::http-client"] 12 | keywords = ["http", "cache", "headers", "cache-control", "proxy"] 13 | include = ["Cargo.toml", "README.md", "src/*.rs", "LICENSE"] 14 | readme = "README.md" 15 | rust-version = "1.64" 16 | 17 | [dependencies] 18 | http = "1.0.0" 19 | http-serde = { version = "2.0.0", optional = true } 20 | serde = { version = "1.0.193", optional = true, features = ["derive"] } 21 | reqwest = { version = "0.12", default-features = false, optional = true } 22 | time = { version = "0.3.20", features = ["parsing", "formatting"] } 23 | 24 | [dev-dependencies] 25 | dialoguer = "0.11.0" 26 | serde_json = "1.0.108" 27 | 28 | [features] 29 | default = ["serde"] 30 | serde = ["dep:serde", "dep:http-serde"] 31 | 32 | [package.metadata.docs.rs] 33 | targets = ["x86_64-unknown-linux-gnu"] 34 | rustdoc-args = ["--generate-link-to-definition"] 35 | 36 | [badges] 37 | maintenance = { status = "passively-maintained" } 38 | -------------------------------------------------------------------------------- /tests/request.rs: -------------------------------------------------------------------------------- 1 | use http::{header, Method, Request, Response}; 2 | use http_cache_semantics::CacheOptions; 3 | use http_cache_semantics::CachePolicy; 4 | use std::time::SystemTime; 5 | 6 | fn public_cacheable_response() -> http::response::Parts { 7 | response_parts(Response::builder().header(header::CACHE_CONTROL, "public, max-age=222")) 8 | } 9 | 10 | fn cacheable_response() -> http::response::Parts { 11 | response_parts(Response::builder().header(header::CACHE_CONTROL, "max-age=111")) 12 | } 13 | 14 | fn request_parts(builder: http::request::Builder) -> http::request::Parts { 15 | builder.body(()).unwrap().into_parts().0 16 | } 17 | 18 | fn response_parts(builder: http::response::Builder) -> http::response::Parts { 19 | builder.body(()).unwrap().into_parts().0 20 | } 21 | 22 | #[test] 23 | fn test_no_store_kills_cache() { 24 | let now = SystemTime::now(); 25 | let policy = CachePolicy::new( 26 | &request_parts( 27 | Request::builder() 28 | .method(Method::GET) 29 | .header(header::CACHE_CONTROL, "no-store"), 30 | ), 31 | &public_cacheable_response(), 32 | ); 33 | 34 | assert!(policy.is_stale(now)); 35 | assert!(!policy.is_storable()); 36 | } 37 | 38 | #[test] 39 | fn test_post_not_cacheable_by_default() { 40 | let now = SystemTime::now(); 41 | let policy = CachePolicy::new( 42 | &request_parts(Request::builder().method(Method::POST)), 43 | &response_parts(Response::builder().header(header::CACHE_CONTROL, "public")), 44 | ); 45 | 46 | assert!(policy.is_stale(now)); 47 | assert!(!policy.is_storable()); 48 | } 49 | 50 | #[test] 51 | fn test_post_cacheable_explicitly() { 52 | let now = SystemTime::now(); 53 | let policy = CachePolicy::new( 54 | &request_parts(Request::builder().method(Method::POST)), 55 | &public_cacheable_response(), 56 | ); 57 | 58 | assert!(!policy.is_stale(now)); 59 | assert!(policy.is_storable()); 60 | } 61 | 62 | #[test] 63 | fn test_public_cacheable_auth_is_ok() { 64 | let now = SystemTime::now(); 65 | let policy = CachePolicy::new( 66 | &request_parts( 67 | Request::builder() 68 | .method(Method::GET) 69 | .header(header::AUTHORIZATION, "test"), 70 | ), 71 | &public_cacheable_response(), 72 | ); 73 | 74 | assert!(!policy.is_stale(now)); 75 | assert!(policy.is_storable()); 76 | } 77 | 78 | #[test] 79 | fn test_private_auth_is_ok() { 80 | let now = SystemTime::now(); 81 | let policy = CachePolicy::new_options( 82 | &request_parts( 83 | Request::builder() 84 | .method(Method::GET) 85 | .header(header::AUTHORIZATION, "test"), 86 | ), 87 | &cacheable_response(), 88 | now, 89 | CacheOptions { 90 | shared: false, 91 | ..Default::default() 92 | }, 93 | ); 94 | 95 | assert!(!policy.is_stale(now)); 96 | assert!(policy.is_storable()); 97 | } 98 | 99 | #[test] 100 | fn test_revalidate_auth_is_ok() { 101 | let policy = CachePolicy::new( 102 | &request_parts( 103 | Request::builder() 104 | .method(Method::GET) 105 | .header(header::AUTHORIZATION, "test"), 106 | ), 107 | &response_parts( 108 | Response::builder().header(header::CACHE_CONTROL, "max-age=88,must-revalidate"), 109 | ), 110 | ); 111 | 112 | assert!(policy.is_storable()); 113 | } 114 | 115 | #[test] 116 | fn test_auth_prevents_caching_by_default() { 117 | let now = SystemTime::now(); 118 | let policy = CachePolicy::new( 119 | &request_parts( 120 | Request::builder() 121 | .method(Method::GET) 122 | .header(header::AUTHORIZATION, "test"), 123 | ), 124 | &cacheable_response(), 125 | ); 126 | 127 | assert!(policy.is_stale(now)); 128 | assert_eq!(policy.is_storable(), false); 129 | } 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Can I cache this? 2 | 3 | `CachePolicy` tells when responses can be reused from a cache, taking into account [HTTP RFC 7234/9111](http://httpwg.org/specs/rfc9111.html) rules for user agents and shared caches. It's aware of many tricky details such as the `Vary` header, age updates, proxy revalidation, and authenticated responses. 4 | 5 | ## Usage 6 | 7 | Cacheability of an HTTP response depends on how it was requested, so both `request` and `response` are required to create the policy. 8 | 9 | It may be surprising, but it's not enough for an HTTP response to be [fresh](#yo-fresh) to satisfy a request. It may need to match request headers specified in `Vary`. Even a matching fresh response may still not be usable if the new request restricted cacheability, etc. 10 | 11 | The key method is `before_request(new_request)`, which checks whether the `new_request` is compatible with the original request and whether all caching conditions are met. 12 | 13 | ### Options 14 | 15 | If `options.shared` is `true` (default), then the response is evaluated from a perspective of a shared cache (i.e. `private` is not cacheable and `s-maxage` is respected). If `options.shared` is `false`, then the response is evaluated from a perspective of a single-user cache (i.e. `private` is cacheable and `s-maxage` is ignored). `shared: true` is recommended for HTTP proxies, and `false` for single-user clients. 16 | 17 | `options.cache_heuristic` is a fraction of response's age that is used as a fallback cache duration. The default is 0.1 (10%), e.g. if a file hasn't been modified for 100 days, it'll be cached for 100×0.1 = 10 days. 18 | 19 | `options.immutable_min_time_to_live` is a duration to assume as the default time to cache responses with `Cache-Control: immutable`. Note that [per RFC](http://httpwg.org/http-extensions/immutable.html) these can become stale, so `max-age` still overrides the default. 20 | 21 | If `options.ignore_cargo_cult` is true, common anti-cache directives will be completely ignored if the non-standard `pre-check` and `post-check` directives are present. These two useless directives are most commonly found in bad StackOverflow answers and PHP's "session limiter" defaults. 22 | 23 | ### `is_storable()` 24 | 25 | Returns `true` if the response can be stored in a cache. If it's `false` then you MUST NOT store either the request or the response. 26 | 27 | ### `before_request(new_request)` 28 | 29 | This is the most important method. Use this method to check whether the cached response is still fresh in the context of the new request. 30 | 31 | If it returns `Fresh`, then the given `request` matches the original response this cache policy has been created with, and the response can be reused without contacting the server. This will contain an updated, filtered set of response headers to return to clients receiving the cached response. This processing is necessary, because proxies MUST always remove hop-by-hop headers (such as `TE` and `Connection`) and update response's `Age` to avoid doubling cache time. 32 | 33 | If it returns `Stale`, then the response may not be matching at all (e.g. it's for a different URL or method), or may require to be refreshed first. The variant will contain HTTP headers for making a revalidation request to the server. 34 | 35 | ### `time_to_live()` 36 | 37 | Returns approximate time until the response becomes stale (i.e. not fresh). This is equivalent of `max-age`, but with appropriate time correction applied. 38 | 39 | After that time (when `time_to_live() == Duration::ZERO`) the response might not be usable without revalidation. However, there are exceptions, e.g. a client can explicitly allow stale responses, so always check with `before_request()`. 40 | 41 | ### Refreshing stale cache (revalidation) 42 | 43 | When a cached response has expired, it can be made fresh again by making a request to the origin server. The server may respond with status 304 (Not Modified) without sending the response body again, saving bandwidth. 44 | 45 | #### `after_response(revalidation_request, revalidation_response)` 46 | 47 | Use this method to update the cache after receiving a new response from the origin server. It returns `Modified`/`NotModified` object with a new `CachePolicy` with HTTP headers updated from `revalidation_response`. You can always replace the old cached `CachePolicy` with the new one. 48 | 49 | - If `NotModified`, then a valid 304 Not Modified response has been received, and you can reuse the old cached response body. 50 | - If `Modified`, you should replace the old cached body with the new response's body. 51 | 52 | # Yo, FRESH 53 | 54 | ![satisfies_without_revalidation](fresh.jpg) 55 | 56 | ## Implemented 57 | 58 | - `Cache-Control` response header with all the quirks. 59 | - `Expires` with check for bad clocks. 60 | - `Pragma` response header. 61 | - `Age` response header. 62 | - `Vary` response header. 63 | - Default cacheability of statuses and methods. 64 | - Requests for stale data. 65 | - Filtering of hop-by-hop headers. 66 | - Basic revalidation request 67 | 68 | ## Unimplemented 69 | 70 | - Merging of range requests, If-Range (but correctly supports them as non-cacheable) 71 | - Revalidation of multiple representations 72 | -------------------------------------------------------------------------------- /tests/update.rs: -------------------------------------------------------------------------------- 1 | use http::header::HeaderName; 2 | use http::request::Parts as RequestParts; 3 | use http::{header, HeaderMap, Request, Response}; 4 | use http_cache_semantics::AfterResponse; 5 | use http_cache_semantics::CachePolicy; 6 | use std::time::Duration; 7 | use std::time::SystemTime; 8 | 9 | fn request_parts(builder: http::request::Builder) -> http::request::Parts { 10 | builder.body(()).unwrap().into_parts().0 11 | } 12 | 13 | fn response_parts(builder: http::response::Builder) -> http::response::Parts { 14 | builder.body(()).unwrap().into_parts().0 15 | } 16 | 17 | fn simple_request_builder_for_update(additional_headers: Option) -> http::request::Builder { 18 | let mut builder = Request::builder() 19 | .header(header::HOST, "www.w3c.org") 20 | .header(header::CONNECTION, "close") 21 | .uri("/Protocols/rfc2616/rfc2616-sec14.html"); 22 | 23 | let builder_headers = builder.headers_mut().unwrap(); 24 | if let Some(h) = additional_headers { 25 | for (key, value) in h { 26 | builder_headers.insert(key.unwrap(), value); 27 | } 28 | } 29 | 30 | builder 31 | } 32 | 33 | fn cacheable_response_builder_for_update() -> http::response::Builder { 34 | Response::builder().header(header::CACHE_CONTROL, "max-age=111") 35 | } 36 | 37 | fn etagged_response_builder() -> http::response::Builder { 38 | cacheable_response_builder_for_update().header(header::ETAG, "\"123456789\"") 39 | } 40 | 41 | fn request_parts_from_headers(headers: HeaderMap) -> RequestParts { 42 | let mut builder = Request::builder(); 43 | 44 | for (key, value) in headers { 45 | match key { 46 | Some(x) => { 47 | builder.headers_mut().unwrap().insert(x, value); 48 | } 49 | None => (), 50 | } 51 | } 52 | 53 | request_parts(builder) 54 | } 55 | 56 | fn not_modified_response_headers_for_update( 57 | first_request_builder: http::request::Builder, 58 | first_response_builder: http::response::Builder, 59 | second_request_builder: http::request::Builder, 60 | second_response_builder: http::response::Builder, 61 | ) -> Option { 62 | let now = SystemTime::now(); 63 | let policy = CachePolicy::new( 64 | &request_parts(first_request_builder), 65 | &response_parts(first_response_builder), 66 | ); 67 | 68 | let headers = get_revalidation_request( 69 | &policy, 70 | &request_parts(second_request_builder), 71 | now + Duration::from_secs(3600 * 24), 72 | ) 73 | .headers; 74 | 75 | let rev = policy.after_response( 76 | &request_parts_from_headers(headers), 77 | &response_parts(second_response_builder), 78 | now, 79 | ); 80 | 81 | match rev { 82 | AfterResponse::Modified(..) => None, 83 | AfterResponse::NotModified(_, res) => Some(res.headers), 84 | } 85 | } 86 | 87 | fn assert_updates( 88 | first_request_builder: http::request::Builder, 89 | first_response_builder: http::response::Builder, 90 | second_request_builder: http::request::Builder, 91 | second_response_builder: http::response::Builder, 92 | ) { 93 | let extended_second_response_builder = second_response_builder 94 | .header(HeaderName::from_static("foo"), "updated") 95 | .header(HeaderName::from_static("x-ignore-new"), "ignoreme"); 96 | let etag_built = extended_second_response_builder 97 | .headers_ref() 98 | .unwrap() 99 | .get(header::ETAG) 100 | .unwrap() 101 | .clone(); 102 | 103 | let headers = not_modified_response_headers_for_update( 104 | first_request_builder, 105 | first_response_builder 106 | .header(HeaderName::from_static("foo"), "original") 107 | .header(HeaderName::from_static("x-other"), "original"), 108 | second_request_builder, 109 | extended_second_response_builder, 110 | ) 111 | .expect("not_modified_response_headers_for_update"); 112 | 113 | assert_eq!(headers.get("foo").unwrap(), "updated"); 114 | assert_eq!(headers.get("x-other").unwrap(), "original"); 115 | assert!(headers.get("x-ignore-new").is_none()); 116 | assert_eq!(headers.get(header::ETAG).unwrap(), etag_built); 117 | } 118 | 119 | #[test] 120 | fn test_matching_etags_are_updated() { 121 | assert_updates( 122 | simple_request_builder_for_update(None), 123 | etagged_response_builder(), 124 | simple_request_builder_for_update(None), 125 | etagged_response_builder().status(http::StatusCode::NOT_MODIFIED), 126 | ); 127 | } 128 | 129 | fn get_revalidation_request( 130 | policy: &CachePolicy, 131 | req: &(impl http_cache_semantics::RequestLike + std::fmt::Debug), 132 | now: SystemTime, 133 | ) -> http::request::Parts { 134 | match policy.before_request(req, now) { 135 | http_cache_semantics::BeforeRequest::Stale { request, matches } => { 136 | if !matches { 137 | eprintln!("warning: req doesn't match {req:#?} vs {policy:?}"); 138 | } 139 | request 140 | } 141 | _ => panic!("no revalidation needed"), 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /tests/satisfy.rs: -------------------------------------------------------------------------------- 1 | use http::{header, Method, Request, Response}; 2 | use http_cache_semantics::CacheOptions; 3 | use http_cache_semantics::CachePolicy; 4 | use std::time::SystemTime; 5 | use time::format_description::well_known::Rfc2822; 6 | use time::Duration; 7 | use time::OffsetDateTime; 8 | 9 | fn request_parts(builder: http::request::Builder) -> http::request::Parts { 10 | builder.body(()).unwrap().into_parts().0 11 | } 12 | 13 | fn response_parts(builder: http::response::Builder) -> http::response::Parts { 14 | builder.body(()).unwrap().into_parts().0 15 | } 16 | 17 | #[test] 18 | fn test_when_urls_match() { 19 | let now = SystemTime::now(); 20 | let response = &response_parts( 21 | Response::builder() 22 | .status(200) 23 | .header(header::CACHE_CONTROL, "max-age=2"), 24 | ); 25 | 26 | let policy = CachePolicy::new(&request_parts(Request::builder().uri("/")), response); 27 | 28 | assert!(policy 29 | .before_request(&mut request_parts(Request::builder().uri("/")), now) 30 | .satisfies_without_revalidation()); 31 | } 32 | 33 | #[test] 34 | fn test_when_expires_is_present() { 35 | let now = SystemTime::now(); 36 | let two_seconds_later = OffsetDateTime::now_utc() 37 | .checked_add(Duration::seconds(2)) 38 | .unwrap() 39 | .format(&Rfc2822) 40 | .unwrap(); 41 | let response = &response_parts( 42 | Response::builder() 43 | .status(302) 44 | .header(header::EXPIRES, two_seconds_later), 45 | ); 46 | 47 | let policy = CachePolicy::new(&request_parts(Request::builder()), response); 48 | 49 | assert!(policy 50 | .before_request(&mut request_parts(Request::builder()), now) 51 | .satisfies_without_revalidation()); 52 | } 53 | 54 | #[test] 55 | fn test_when_methods_match() { 56 | let now = SystemTime::now(); 57 | let response = &response_parts( 58 | Response::builder() 59 | .status(200) 60 | .header(header::CACHE_CONTROL, "max-age=2"), 61 | ); 62 | let policy = CachePolicy::new( 63 | &request_parts(Request::builder().method(Method::GET)), 64 | response, 65 | ); 66 | 67 | assert!(policy 68 | .before_request(&request_parts(Request::builder().method(Method::GET)), now) 69 | .satisfies_without_revalidation()); 70 | } 71 | 72 | #[test] 73 | fn must_revalidate_allows_not_revalidating_fresh() { 74 | let now = SystemTime::now(); 75 | let response = &response_parts( 76 | Response::builder() 77 | .status(200) 78 | .header(header::CACHE_CONTROL, "max-age=200, must-revalidate"), 79 | ); 80 | let policy = CachePolicy::new( 81 | &request_parts(Request::builder().method(Method::GET)), 82 | response, 83 | ); 84 | 85 | assert!(policy 86 | .before_request(&request_parts(Request::builder().method(Method::GET)), now) 87 | .satisfies_without_revalidation()); 88 | 89 | let later = now + std::time::Duration::from_secs(300); 90 | assert!(!policy 91 | .before_request( 92 | &request_parts(Request::builder().method(Method::GET)), 93 | later 94 | ) 95 | .satisfies_without_revalidation()); 96 | } 97 | 98 | #[test] 99 | fn must_revalidate_disallows_stale() { 100 | let now = SystemTime::now(); 101 | let response = &response_parts( 102 | Response::builder() 103 | .status(200) 104 | .header(header::CACHE_CONTROL, "max-age=200, must-revalidate"), 105 | ); 106 | let policy = CachePolicy::new( 107 | &request_parts(Request::builder().method(Method::GET)), 108 | response, 109 | ); 110 | 111 | let later = now + std::time::Duration::from_secs(300); 112 | assert!(!policy 113 | .before_request( 114 | &request_parts(Request::builder().method(Method::GET)), 115 | later 116 | ) 117 | .satisfies_without_revalidation()); 118 | 119 | let later = now + std::time::Duration::from_secs(300); 120 | assert!(!policy 121 | .before_request( 122 | &request_parts( 123 | Request::builder() 124 | .header("cache-control", "max-stale") 125 | .method(Method::GET) 126 | ), 127 | later 128 | ) 129 | .satisfies_without_revalidation()); 130 | } 131 | 132 | #[test] 133 | fn test_not_when_hosts_mismatch() { 134 | let now = SystemTime::now(); 135 | let response = &response_parts( 136 | Response::builder() 137 | .status(200) 138 | .header(header::CACHE_CONTROL, "max-age=2"), 139 | ); 140 | let policy = CachePolicy::new( 141 | &request_parts(Request::builder().header(header::HOST, "foo")), 142 | response, 143 | ); 144 | 145 | assert!(policy 146 | .before_request( 147 | &request_parts(Request::builder().header(header::HOST, "foo")), 148 | now 149 | ) 150 | .satisfies_without_revalidation()); 151 | 152 | assert!(!policy 153 | .before_request( 154 | &request_parts(Request::builder().header(header::HOST, "foofoo")), 155 | now 156 | ) 157 | .satisfies_without_revalidation()); 158 | } 159 | 160 | #[test] 161 | fn test_when_methods_match_head() { 162 | let now = SystemTime::now(); 163 | let response = &response_parts( 164 | Response::builder() 165 | .status(200) 166 | .header(header::CACHE_CONTROL, "max-age=2"), 167 | ); 168 | let policy = CachePolicy::new( 169 | &request_parts(Request::builder().method(Method::HEAD)), 170 | response, 171 | ); 172 | 173 | assert!(policy 174 | .before_request(&request_parts(Request::builder().method(Method::HEAD)), now) 175 | .satisfies_without_revalidation()); 176 | } 177 | 178 | #[test] 179 | fn test_not_when_proxy_revalidating() { 180 | let now = SystemTime::now(); 181 | let response = &response_parts( 182 | Response::builder() 183 | .status(200) 184 | .header(header::CACHE_CONTROL, "max-age=2, proxy-revalidate "), 185 | ); 186 | let policy = CachePolicy::new(&request_parts(Request::builder()), response); 187 | 188 | assert!(!policy 189 | .before_request(&mut request_parts(Request::builder()), now) 190 | .satisfies_without_revalidation()); 191 | } 192 | 193 | #[test] 194 | fn test_when_not_a_proxy_revalidating() { 195 | let now = SystemTime::now(); 196 | let response = &response_parts( 197 | Response::builder() 198 | .status(200) 199 | .header(header::CACHE_CONTROL, "max-age=2, proxy-revalidate "), 200 | ); 201 | let policy = CachePolicy::new_options( 202 | &request_parts(Request::builder()), 203 | response, 204 | now, 205 | CacheOptions { 206 | shared: false, 207 | ..Default::default() 208 | }, 209 | ); 210 | 211 | assert!(policy 212 | .before_request(&mut request_parts(Request::builder()), now) 213 | .satisfies_without_revalidation()); 214 | } 215 | 216 | #[test] 217 | fn test_not_when_no_cache_requesting() { 218 | let now = SystemTime::now(); 219 | let response = &response_parts(Response::builder().header(header::CACHE_CONTROL, "max-age=2")); 220 | let policy = CachePolicy::new(&request_parts(Request::builder()), response); 221 | 222 | assert!(policy 223 | .before_request( 224 | &request_parts(Request::builder().header(header::CACHE_CONTROL, "fine")), 225 | now 226 | ) 227 | .satisfies_without_revalidation()); 228 | 229 | assert!(!policy 230 | .before_request( 231 | &request_parts(Request::builder().header(header::CACHE_CONTROL, "no-cache")), 232 | now 233 | ) 234 | .satisfies_without_revalidation()); 235 | 236 | assert!(!policy 237 | .before_request( 238 | &request_parts(Request::builder().header(header::PRAGMA, "no-cache")), 239 | now 240 | ) 241 | .satisfies_without_revalidation()); 242 | } 243 | -------------------------------------------------------------------------------- /tests/vary.rs: -------------------------------------------------------------------------------- 1 | use http::{header, Request, Response}; 2 | use http_cache_semantics::CachePolicy; 3 | 4 | use std::time::SystemTime; 5 | 6 | fn request_parts(builder: http::request::Builder) -> http::request::Parts { 7 | builder.body(()).unwrap().into_parts().0 8 | } 9 | 10 | fn response_parts(builder: http::response::Builder) -> http::response::Parts { 11 | builder.body(()).unwrap().into_parts().0 12 | } 13 | 14 | #[test] 15 | fn test_vary_basic() { 16 | let now = SystemTime::now(); 17 | let response = response_parts( 18 | Response::builder() 19 | .header(header::CACHE_CONTROL, "max-age=5") 20 | .header(header::VARY, "weather"), 21 | ); 22 | 23 | let policy = CachePolicy::new( 24 | &request_parts(Request::builder().header("weather", "nice")), 25 | &response, 26 | ); 27 | 28 | assert!(policy 29 | .before_request( 30 | &request_parts(Request::builder().header("weather", "nice")), 31 | now 32 | ) 33 | .satisfies_without_revalidation()); 34 | 35 | assert!(!policy 36 | .before_request( 37 | &request_parts(Request::builder().header("weather", "bad")), 38 | now 39 | ) 40 | .satisfies_without_revalidation()); 41 | } 42 | 43 | #[test] 44 | fn test_asterisks_does_not_match() { 45 | let now = SystemTime::now(); 46 | let response = response_parts( 47 | Response::builder() 48 | .header(header::CACHE_CONTROL, "max-age=5") 49 | .header(header::VARY, "*"), 50 | ); 51 | 52 | let policy = CachePolicy::new( 53 | &request_parts(Request::builder().header("weather", "ok")), 54 | &response, 55 | ); 56 | 57 | assert!(!policy 58 | .before_request( 59 | &request_parts(Request::builder().header("weather", "ok")), 60 | now 61 | ) 62 | .satisfies_without_revalidation()); 63 | } 64 | 65 | #[test] 66 | fn test_asterisks_is_stale() { 67 | let now = SystemTime::now(); 68 | let policy_one = CachePolicy::new( 69 | &request_parts(Request::builder().header("weather", "ok")), 70 | &response_parts( 71 | Response::builder() 72 | .header(header::CACHE_CONTROL, "public,max-age=99") 73 | .header(header::VARY, "*"), 74 | ), 75 | ); 76 | 77 | let policy_two = CachePolicy::new( 78 | &request_parts(Request::builder().header("weather", "ok")), 79 | &response_parts( 80 | Response::builder() 81 | .header(header::CACHE_CONTROL, "public,max-age=99") 82 | .header(header::VARY, "weather"), 83 | ), 84 | ); 85 | 86 | assert!(policy_one.is_stale(now)); 87 | assert!(!policy_two.is_stale(now)); 88 | } 89 | 90 | #[test] 91 | fn test_values_are_case_sensitive() { 92 | let now = SystemTime::now(); 93 | let response = response_parts( 94 | Response::builder() 95 | .header(header::CACHE_CONTROL, "public,max-age=5") 96 | .header(header::VARY, "weather"), 97 | ); 98 | 99 | let policy = CachePolicy::new( 100 | &request_parts(Request::builder().header("weather", "BAD")), 101 | &response, 102 | ); 103 | 104 | assert!(policy 105 | .before_request( 106 | &request_parts(Request::builder().header("weather", "BAD")), 107 | now 108 | ) 109 | .satisfies_without_revalidation()); 110 | 111 | assert!(!policy 112 | .before_request( 113 | &request_parts(Request::builder().header("weather", "bad")), 114 | now 115 | ) 116 | .satisfies_without_revalidation()); 117 | } 118 | 119 | #[test] 120 | fn test_irrelevant_headers_ignored() { 121 | let now = SystemTime::now(); 122 | let response = response_parts( 123 | Response::builder() 124 | .header(header::CACHE_CONTROL, "max-age=5") 125 | .header(header::VARY, "moon-phase"), 126 | ); 127 | 128 | let policy = CachePolicy::new( 129 | &request_parts(Request::builder().header("weather", "nice")), 130 | &response, 131 | ); 132 | 133 | assert!(policy 134 | .before_request( 135 | &request_parts(Request::builder().header("weather", "bad")), 136 | now 137 | ) 138 | .satisfies_without_revalidation()); 139 | 140 | assert!(policy 141 | .before_request( 142 | &request_parts(Request::builder().header("weather", "shining")), 143 | now 144 | ) 145 | .satisfies_without_revalidation()); 146 | 147 | assert!(!policy 148 | .before_request( 149 | &request_parts(Request::builder().header("moon-phase", "full")), 150 | now 151 | ) 152 | .satisfies_without_revalidation()); 153 | } 154 | 155 | #[test] 156 | fn test_absence_is_meaningful() { 157 | let now = SystemTime::now(); 158 | let response = response_parts( 159 | Response::builder() 160 | .header(header::CACHE_CONTROL, "max-age=5") 161 | .header(header::VARY, "moon-phase, weather"), 162 | ); 163 | 164 | let policy = CachePolicy::new( 165 | &request_parts(Request::builder().header("weather", "nice")), 166 | &response, 167 | ); 168 | 169 | assert!(policy 170 | .before_request( 171 | &request_parts(Request::builder().header("weather", "nice")), 172 | now, 173 | ) 174 | .satisfies_without_revalidation()); 175 | 176 | assert!(!policy 177 | .before_request( 178 | &request_parts( 179 | Request::builder() 180 | .header("weather", "nice") 181 | .header("moon-phase", "") 182 | ), 183 | now, 184 | ) 185 | .satisfies_without_revalidation()); 186 | 187 | assert!(!policy 188 | .before_request(&request_parts(Request::builder()), now) 189 | .satisfies_without_revalidation()); 190 | } 191 | 192 | #[test] 193 | fn test_all_values_must_match() { 194 | let now = SystemTime::now(); 195 | let response = response_parts( 196 | Response::builder() 197 | .header(header::CACHE_CONTROL, "max-age=5") 198 | .header(header::VARY, "weather, sun"), 199 | ); 200 | 201 | let policy = CachePolicy::new( 202 | &request_parts( 203 | Request::builder() 204 | .header("sun", "shining") 205 | .header("weather", "nice"), 206 | ), 207 | &response, 208 | ); 209 | 210 | assert!(policy 211 | .before_request( 212 | &request_parts( 213 | Request::builder() 214 | .header("sun", "shining") 215 | .header("weather", "nice") 216 | ), 217 | now 218 | ) 219 | .satisfies_without_revalidation()); 220 | 221 | assert!(!policy 222 | .before_request( 223 | &request_parts( 224 | Request::builder() 225 | .header("sun", "shining") 226 | .header("weather", "bad") 227 | ), 228 | now 229 | ) 230 | .satisfies_without_revalidation()); 231 | } 232 | 233 | #[test] 234 | fn test_whitespace_is_okay() { 235 | let now = SystemTime::now(); 236 | let response = response_parts( 237 | Response::builder() 238 | .header(header::CACHE_CONTROL, "max-age=5") 239 | .header(header::VARY, " weather , sun "), 240 | ); 241 | 242 | let policy = CachePolicy::new( 243 | &request_parts( 244 | Request::builder() 245 | .header("sun", "shining") 246 | .header("weather", "nice"), 247 | ), 248 | &response, 249 | ); 250 | 251 | assert!(policy 252 | .before_request( 253 | &request_parts( 254 | Request::builder() 255 | .header("sun", "shining") 256 | .header("weather", "nice") 257 | ), 258 | now 259 | ) 260 | .satisfies_without_revalidation()); 261 | 262 | assert!(!policy 263 | .before_request( 264 | &request_parts(Request::builder().header("weather", "nice")), 265 | now 266 | ) 267 | .satisfies_without_revalidation()); 268 | 269 | assert!(!policy 270 | .before_request( 271 | &request_parts(Request::builder().header("sun", "shining")), 272 | now 273 | ) 274 | .satisfies_without_revalidation()); 275 | } 276 | 277 | #[test] 278 | fn test_order_is_irrelevant() { 279 | let now = SystemTime::now(); 280 | let response_one = response_parts( 281 | Response::builder() 282 | .header(header::CACHE_CONTROL, "max-age=5") 283 | .header(header::VARY, "weather, sun"), 284 | ); 285 | 286 | let response_two = response_parts( 287 | Response::builder() 288 | .header(header::CACHE_CONTROL, "max-age=5") 289 | .header(header::VARY, "sun, weather"), 290 | ); 291 | 292 | let policy_one = CachePolicy::new( 293 | &request_parts( 294 | Request::builder() 295 | .header("sun", "shining") 296 | .header("weather", "nice"), 297 | ), 298 | &response_one, 299 | ); 300 | 301 | let policy_two = CachePolicy::new( 302 | &request_parts( 303 | Request::builder() 304 | .header("sun", "shining") 305 | .header("weather", "nice"), 306 | ), 307 | &response_two, 308 | ); 309 | 310 | assert!(policy_one 311 | .before_request( 312 | &request_parts( 313 | Request::builder() 314 | .header("weather", "nice") 315 | .header("sun", "shining") 316 | ), 317 | now 318 | ) 319 | .satisfies_without_revalidation()); 320 | 321 | assert!(policy_one 322 | .before_request( 323 | &request_parts( 324 | Request::builder() 325 | .header("sun", "shining") 326 | .header("weather", "nice") 327 | ), 328 | now 329 | ) 330 | .satisfies_without_revalidation()); 331 | 332 | assert!(policy_two 333 | .before_request( 334 | &request_parts( 335 | Request::builder() 336 | .header("weather", "nice") 337 | .header("sun", "shining") 338 | ), 339 | now 340 | ) 341 | .satisfies_without_revalidation()); 342 | 343 | assert!(policy_two 344 | .before_request( 345 | &request_parts( 346 | Request::builder() 347 | .header("sun", "shining") 348 | .header("weather", "nice") 349 | ), 350 | now 351 | ) 352 | .satisfies_without_revalidation()); 353 | } 354 | -------------------------------------------------------------------------------- /examples/interactive.rs: -------------------------------------------------------------------------------- 1 | //! A command-line interactive HTTP cache demo 2 | //! 3 | //! All of the `http_cache_semantics` logic is contained entirely within `fn make_a_request()` 4 | 5 | use std::{collections::HashMap, sync::{LazyLock, Mutex}, time::{Duration, SystemTime}}; 6 | 7 | use dialoguer::{console::style, theme::ColorfulTheme, Input}; 8 | use http::{Response, Request, Uri}; 9 | use http_cache_semantics::{CacheOptions, CachePolicy}; 10 | 11 | const START: SystemTime = SystemTime::UNIX_EPOCH; 12 | static CURRENT_TIME: Mutex = Mutex::new(START); 13 | static THEME: LazyLock = LazyLock::new(ColorfulTheme::default); 14 | 15 | type Req = Request<()>; 16 | type Body = String; 17 | type Resp = Response; 18 | type Cache = HashMap; 19 | 20 | fn main() { 21 | // handle cli args 22 | let mut args = std::env::args(); 23 | let _exe = args.next().unwrap(); 24 | let has_private_flag = match args.next().as_deref() { 25 | None => { 26 | println!( 27 | "running as a {}. pass {} to run as a private cache", 28 | bold("shared cache").magenta(), 29 | style("`-- --private-cache`").dim().italic(), 30 | ); 31 | false 32 | } 33 | Some("-p" | "--private-cache") => { 34 | println!("running as a {}", bold("private cache").blue()); 35 | true 36 | } 37 | _ => { 38 | eprintln!("usage: cargo run --example=interactive -- [-p|--private-cache]"); 39 | std::process::exit(1); 40 | } 41 | }; 42 | 43 | let cache_options = CacheOptions { 44 | shared: !has_private_flag, 45 | ..Default::default() 46 | }; 47 | let mut cache = Cache::new(); 48 | let items = ["make a request", "advance time", "list cache entries", "quit"]; 49 | loop { 50 | println!("{} {}", bold("current time:"), style(current_m_ss()).green()); 51 | let selection = select_prompt() 52 | .with_prompt("pick an action") 53 | .items(&items) 54 | .interact() 55 | .unwrap(); 56 | match selection { 57 | 0 => make_a_request(&mut cache, cache_options), 58 | 1 => advance_time(), 59 | 2 => list_cache_entries(&cache), 60 | 3 => break, 61 | _ => unreachable!(), 62 | } 63 | } 64 | 65 | println!("\n...and a peek at the cache to finish things off. goodbye!"); 66 | list_cache_entries(&cache); 67 | } 68 | 69 | fn make_a_request(cache: &mut Cache, cache_options: CacheOptions) { 70 | use std::collections::hash_map::Entry; 71 | 72 | use http_cache_semantics::{AfterResponse, BeforeRequest}; 73 | 74 | let req = setup_req(); 75 | let resp = match cache.entry(req.uri().to_owned()) { 76 | Entry::Occupied(mut occupied) => { 77 | let (policy, body) = occupied.get(); 78 | match policy.before_request(&req, current_time()) { 79 | BeforeRequest::Fresh(resp) => { 80 | println!("{} retrieving cached response", bold("fresh cache entry!").green()); 81 | Resp::from_parts(resp, body.to_owned()) 82 | }, 83 | BeforeRequest::Stale { request, .. } => { 84 | println!("{}", bold("stale entry!").red()); 85 | let new_req = Req::from_parts(request, ()); 86 | let mut resp = server::get(new_req.clone()); 87 | let after_resp = policy.after_response(&new_req, &resp, current_time()); 88 | let (not_modified, new_policy, new_resp) = match after_resp { 89 | AfterResponse::NotModified(p, r) => (true, p, r), 90 | AfterResponse::Modified(p, r) => (false, p, r), 91 | }; 92 | // NOTE: if the policy isn't storable then you MUST NOT store the entry 93 | if new_policy.is_storable() { 94 | if not_modified { 95 | println!("{} only updating metadata", bold("not modified!").blue()); 96 | let entry = occupied.get_mut(); 97 | entry.0 = new_policy; 98 | // and reconstruct the response from our cached bits 99 | resp = Resp::from_parts(new_resp, entry.1.clone()); 100 | } else { 101 | println!("{} updating full entry", bold("modified!").magenta()); 102 | occupied.insert((new_policy, resp.body().to_owned())); 103 | } 104 | } else { 105 | println!( 106 | "{} entry was not considered storable", 107 | bold("skipping cache!").red(), 108 | ); 109 | } 110 | resp 111 | } 112 | } 113 | } 114 | Entry::Vacant(vacant) => { 115 | let resp = server::get(req.clone()); 116 | let policy = CachePolicy::new_options(&req, &resp, current_time(), cache_options); 117 | // NOTE: if the policy isn't storable then you MUST NOT store the entry 118 | if policy.is_storable() { 119 | println!("{} inserting entry", bold("cached!").green()); 120 | let body = resp.body().to_owned(); 121 | vacant.insert((policy, body)); 122 | } else { 123 | println!("{} entry was not considered storable", bold("skipping cache!").red()); 124 | } 125 | resp 126 | } 127 | }; 128 | 129 | println!("\n{} {} {}", bold("response from -"), bold("GET").green(), style(req.uri()).green()); 130 | println!("{}", bold("headers -").blue()); 131 | for (name, value) in resp.headers() { 132 | println!("{}: {}", bold(name.as_str()).blue(), style(value.to_str().unwrap()).italic()); 133 | } 134 | println!("{} {}\n", bold("body -").blue(), style(resp.body()).blue()); 135 | } 136 | 137 | fn advance_time() { 138 | let seconds: u64 = Input::with_theme(&*THEME) 139 | .with_prompt("seconds to advance") 140 | .interact() 141 | .unwrap(); 142 | *CURRENT_TIME.lock().unwrap() += Duration::from_secs(seconds); 143 | println!("{} {}", bold("advanced to:"), style(current_m_ss()).green()); 144 | } 145 | 146 | fn list_cache_entries(cache: &Cache) { 147 | println!(); 148 | for (uri, (policy, body)) in cache { 149 | let (stale_msg, ttl) = if policy.is_stale(current_time()) { 150 | (bold("stale").magenta(), style("ttl - expired".to_owned()).italic()) 151 | } else { 152 | let ttl = format!("ttl - {:>7?}", policy.time_to_live(current_time())); 153 | (bold("fresh").blue(), bold(ttl)) 154 | }; 155 | let get = bold("GET").green(); 156 | let uri = style(uri.to_string()).green(); 157 | let body = style(body).blue(); 158 | println!("{stale_msg} {ttl} {get} {uri:23} | {body}"); 159 | } 160 | println!(); 161 | } 162 | 163 | use helpers::{bold, current_duration, current_time, current_m_ss, select_prompt, setup_req}; 164 | mod helpers { 165 | use std::time::{Duration, SystemTime}; 166 | 167 | use super::{CURRENT_TIME, START, THEME, Req}; 168 | 169 | use dialoguer::{console::{style, StyledObject}, Select, }; 170 | 171 | pub fn select_prompt() -> Select<'static> { 172 | Select::with_theme(&*THEME) 173 | .default(0) 174 | } 175 | 176 | pub fn bold(d: D) -> StyledObject { 177 | style(d).bold() 178 | } 179 | 180 | pub fn current_time() -> SystemTime { 181 | *CURRENT_TIME.lock().unwrap() 182 | } 183 | 184 | pub fn current_duration() -> Duration { 185 | current_time().duration_since(START).unwrap() 186 | } 187 | 188 | pub fn current_m_ss() -> String { 189 | let elapsed = current_duration(); 190 | let mins = elapsed.as_secs() / 60; 191 | let secs = elapsed.as_secs() % 60; 192 | format!("{mins}m{secs:02}s") 193 | } 194 | 195 | pub fn setup_req() -> Req { 196 | let path_to_cache_desc = [ 197 | ("/current-time", "no-store"), 198 | ("/cached-current-time", "max-age: 10s"), 199 | ("/friends-online", "private, max-age: 30s"), 200 | ("/user/123/profile-pic", "e-tag w/ max-age: 30s"), 201 | ("/cache-busted-123B-8E2A", "immutable"), 202 | ]; 203 | let styled: Vec<_> = path_to_cache_desc 204 | .iter() 205 | .map(|(path, cache_desc)| { 206 | format!( 207 | "{} {:23} {}", 208 | bold("GET").green(), 209 | style(path).green(), 210 | style(format!("server-side - {cache_desc}")).dim().italic(), 211 | ) 212 | }).collect(); 213 | let selection = select_prompt() 214 | .with_prompt("make a request") 215 | .items(&styled) 216 | .interact() 217 | .unwrap(); 218 | let path = path_to_cache_desc[selection].0; 219 | Req::get(path).body(()).unwrap() 220 | } 221 | } 222 | 223 | mod server { 224 | use super::{CURRENT_TIME, START, Resp, Req, bold, current_duration}; 225 | 226 | use dialoguer::console::style; 227 | use http::{header, Response, HeaderValue}; 228 | 229 | pub fn get(req: Req) -> Resp { 230 | println!("{}ing a response for {}", bold("GET").green(), style(req.uri()).green()); 231 | let elapsed = CURRENT_TIME.lock().unwrap().duration_since(START).unwrap(); 232 | match req.uri().path() { 233 | "/current-time" => Response::builder() 234 | .header(header::CACHE_CONTROL, HeaderValue::from_static("no-store")) 235 | .body(format!("current elapsed time {elapsed:?}")), 236 | "/cached-current-time" => Response::builder() 237 | .header(header::CACHE_CONTROL, HeaderValue::from_static("max-age=10")) 238 | .body(format!("cached current elapsed time {elapsed:?}")), 239 | "/friends-online" => { 240 | let randomish_num = (current_duration().as_secs() / 10 + 1) * 1997 % 15; 241 | Response::builder() 242 | .header(header::CACHE_CONTROL, HeaderValue::from_static("private, max-age=30")) 243 | .body(format!("{randomish_num} friends online")) 244 | } 245 | "/user/123/profile-pic" => { 246 | // picture that changes every 5 minutes 247 | let maybe_client_e_tag = req.headers().get(header::IF_NONE_MATCH); 248 | let (pic, e_tag) = match current_duration().as_secs() / 300 % 3 { 249 | 0 => ("(cat looking at stars.jpg)", "1234-abcd"), 250 | 1 => ("(mountainside.png)", "aaaa-ffff"), 251 | 2 => ("(beach sunset.jpeg)", "9c31-be74"), 252 | _ => unreachable!(), 253 | }; 254 | if maybe_client_e_tag.is_some_and(|client_e_tag| client_e_tag == e_tag) { 255 | // handle ETag revalidation 256 | Response::builder() 257 | .header(header::ETAG, HeaderValue::from_str(e_tag).unwrap()) 258 | .status(http::StatusCode::NOT_MODIFIED) 259 | .body("".into()) 260 | } else { 261 | // no valid ETag. send the full response 262 | Response::builder() 263 | .header(header::CACHE_CONTROL, "max-age=30") 264 | .header(header::ETAG, HeaderValue::from_str(e_tag).unwrap()) 265 | .body(pic.to_owned()) 266 | } 267 | } 268 | "/cache-busted-123B-8E2A" => Response::builder() 269 | .header(header::CACHE_CONTROL, HeaderValue::from_static("immutable")) 270 | .body("(pretend like this is some very large asset ~('-')~)".to_owned()), 271 | _ => unreachable!(), 272 | } 273 | .unwrap() 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /tests/okhttp.rs: -------------------------------------------------------------------------------- 1 | use http::{header, HeaderValue, Request, Response}; 2 | use http_cache_semantics::CacheOptions; 3 | use http_cache_semantics::CachePolicy; 4 | use std::time::SystemTime; 5 | use time::format_description::well_known::Rfc2822; 6 | use time::OffsetDateTime; 7 | 8 | fn request_parts(builder: http::request::Builder) -> http::request::Parts { 9 | builder.body(()).unwrap().into_parts().0 10 | } 11 | 12 | fn response_parts(builder: http::response::Builder) -> http::response::Parts { 13 | builder.body(()).unwrap().into_parts().0 14 | } 15 | 16 | fn assert_cached(should_put: bool, response_code: u16) { 17 | let now = SystemTime::now(); 18 | let options = CacheOptions { 19 | shared: false, 20 | ..Default::default() 21 | }; 22 | 23 | let mut response = response_parts( 24 | Response::builder() 25 | .header(header::LAST_MODIFIED, format_date(-105, 1)) 26 | .header(header::EXPIRES, format_date(1, 3600)) 27 | .header(header::WWW_AUTHENTICATE, "challenge") 28 | .status(response_code), 29 | ); 30 | 31 | if 407 == response_code { 32 | response.headers.insert( 33 | header::PROXY_AUTHENTICATE, 34 | HeaderValue::from_static("Basic realm=\"protected area\""), 35 | ); 36 | } else if 401 == response_code { 37 | response.headers.insert( 38 | header::WWW_AUTHENTICATE, 39 | HeaderValue::from_static("Basic realm=\"protected area\""), 40 | ); 41 | } 42 | 43 | let request = request_parts(Request::get("/")); 44 | 45 | let policy = CachePolicy::new_options(&request, &response, now, options); 46 | 47 | assert_eq!(should_put, policy.is_storable()); 48 | } 49 | 50 | #[test] 51 | fn test_ok_http_response_caching_by_response_code() { 52 | assert_cached(false, 100); 53 | assert_cached(false, 101); 54 | assert_cached(false, 102); 55 | assert_cached(true, 200); 56 | assert_cached(false, 201); 57 | assert_cached(false, 202); 58 | assert_cached(true, 203); 59 | assert_cached(true, 204); 60 | assert_cached(false, 205); 61 | // 206: electing to not cache partial responses 62 | assert_cached(false, 206); 63 | assert_cached(false, 207); 64 | assert_cached(true, 300); 65 | assert_cached(true, 301); 66 | assert_cached(true, 302); 67 | assert_cached(false, 304); 68 | assert_cached(false, 305); 69 | assert_cached(false, 306); 70 | assert_cached(true, 307); 71 | assert_cached(true, 308); 72 | assert_cached(false, 400); 73 | assert_cached(false, 401); 74 | assert_cached(false, 402); 75 | assert_cached(false, 403); 76 | assert_cached(true, 404); 77 | assert_cached(true, 405); 78 | assert_cached(false, 406); 79 | assert_cached(false, 408); 80 | assert_cached(false, 409); 81 | // 410: the HTTP spec permits caching 410s, but the RI doesn't 82 | assert_cached(true, 410); 83 | assert_cached(false, 411); 84 | assert_cached(false, 412); 85 | assert_cached(false, 413); 86 | assert_cached(true, 414); 87 | assert_cached(false, 415); 88 | assert_cached(false, 416); 89 | assert_cached(false, 417); 90 | assert_cached(false, 418); 91 | assert_cached(false, 429); 92 | assert_cached(false, 500); 93 | assert_cached(true, 501); 94 | assert_cached(false, 502); 95 | assert_cached(false, 503); 96 | assert_cached(false, 504); 97 | assert_cached(false, 505); 98 | assert_cached(false, 506); 99 | } 100 | 101 | #[test] 102 | fn test_default_expiration_date_fully_cached_for_less_than_24_hours() { 103 | let now = SystemTime::now(); 104 | let options = CacheOptions { 105 | shared: false, 106 | ..Default::default() 107 | }; 108 | 109 | let policy = CachePolicy::new_options( 110 | &request_parts(Request::get("/")), 111 | &response_parts( 112 | Response::builder() 113 | .header(header::LAST_MODIFIED, format_date(-105, 1)) 114 | .header(header::DATE, format_date(-5, 1)), 115 | ), 116 | now, 117 | options, 118 | ); 119 | 120 | assert!(policy.time_to_live(now).as_millis() > 4000); 121 | } 122 | 123 | #[test] 124 | fn test_default_expiration_date_fully_cached_for_more_than_24_hours() { 125 | let now = SystemTime::now(); 126 | let options = CacheOptions { 127 | shared: false, 128 | ..Default::default() 129 | }; 130 | 131 | let policy = CachePolicy::new_options( 132 | &request_parts(Request::get("/")), 133 | &response_parts( 134 | Response::builder() 135 | .header(header::LAST_MODIFIED, format_date(-105, 3600 * 24)) 136 | .header(header::DATE, format_date(-5, 3600 * 24)), 137 | ), 138 | now, 139 | options, 140 | ); 141 | 142 | assert!((policy.time_to_live(now) + policy.age(now)).as_secs() >= 10 * 3600 * 24); 143 | assert!(policy.time_to_live(now).as_millis() + 1000 >= 5 * 3600 * 24); 144 | } 145 | 146 | #[test] 147 | fn test_max_age_in_the_past_with_date_header_but_no_last_modified_header() { 148 | let now = SystemTime::now(); 149 | let options = CacheOptions { 150 | shared: false, 151 | ..Default::default() 152 | }; 153 | 154 | // Chrome interprets max-age relative to the local clock. Both our cache 155 | // and Firefox both use the earlier of the local and server's clock. 156 | let request = request_parts(Request::get("/")); 157 | let response = response_parts( 158 | Response::builder() 159 | .header(header::AGE, 120) 160 | .header(header::CACHE_CONTROL, "max-age=60"), 161 | ); 162 | let policy = CachePolicy::new_options(&request, &response, now, options); 163 | 164 | assert!(policy.is_stale(now)); 165 | } 166 | 167 | #[test] 168 | fn test_max_age_preferred_over_lower_shared_max_age() { 169 | let now = SystemTime::now(); 170 | let options = CacheOptions { 171 | shared: false, 172 | ..Default::default() 173 | }; 174 | 175 | let policy = CachePolicy::new_options( 176 | &request_parts(Request::builder()), 177 | &response_parts( 178 | Response::builder() 179 | .header(header::DATE, format_date(-2, 60)) 180 | .header(header::CACHE_CONTROL, "s-maxage=60, max-age=180"), 181 | ), 182 | now, 183 | options, 184 | ); 185 | 186 | assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 180); 187 | } 188 | 189 | #[test] 190 | fn test_max_age_preferred_over_higher_max_age() { 191 | let now = SystemTime::now(); 192 | let options = CacheOptions { 193 | shared: false, 194 | ..Default::default() 195 | }; 196 | 197 | let request = request_parts(Request::get("/")); 198 | let response = response_parts( 199 | Response::builder() 200 | .header(header::AGE, 3 * 60) 201 | .header(header::CACHE_CONTROL, "s-maxage=60, max-age=180"), 202 | ); 203 | let policy = CachePolicy::new_options(&request, &response, now, options); 204 | 205 | assert!(policy.is_stale(now)); 206 | } 207 | 208 | fn request_method_not_cached(method: &str) { 209 | let now = SystemTime::now(); 210 | let options = CacheOptions { 211 | shared: false, 212 | ..Default::default() 213 | }; 214 | 215 | // 1. seed the cache (potentially) 216 | // 2. expect a cache hit or miss 217 | let request = request_parts(Request::builder().method(method)); 218 | 219 | let response = 220 | response_parts(Response::builder().header(header::EXPIRES, format_date(1, 3600))); 221 | 222 | let policy = CachePolicy::new_options(&request, &response, now, options); 223 | 224 | assert!(policy.is_stale(now)); 225 | } 226 | 227 | #[test] 228 | fn test_request_method_options_is_not_cached() { 229 | request_method_not_cached("OPTIONS"); 230 | } 231 | 232 | #[test] 233 | fn test_request_method_put_is_not_cached() { 234 | request_method_not_cached("PUT"); 235 | } 236 | 237 | #[test] 238 | fn test_request_method_delete_is_not_cached() { 239 | request_method_not_cached("DELETE"); 240 | } 241 | 242 | #[test] 243 | fn test_request_method_trace_is_not_cached() { 244 | request_method_not_cached("TRACE"); 245 | } 246 | 247 | #[test] 248 | fn test_etag_and_expiration_date_in_the_future() { 249 | let now = SystemTime::now(); 250 | let options = CacheOptions { 251 | shared: false, 252 | ..Default::default() 253 | }; 254 | 255 | let policy = CachePolicy::new_options( 256 | &request_parts(Request::builder()), 257 | &response_parts( 258 | Response::builder() 259 | .header(header::ETAG, "v1") 260 | .header(header::LAST_MODIFIED, format_date(-2, 3600)) 261 | .header(header::EXPIRES, format_date(1, 3600)), 262 | ), 263 | now, 264 | options, 265 | ); 266 | 267 | assert!(policy.time_to_live(now).as_millis() > 0); 268 | } 269 | 270 | #[test] 271 | fn test_client_side_no_store() { 272 | let now = SystemTime::now(); 273 | let options = CacheOptions { 274 | shared: false, 275 | ..Default::default() 276 | }; 277 | 278 | let policy = CachePolicy::new_options( 279 | &request_parts(Request::builder().header(header::CACHE_CONTROL, "no-store")), 280 | &response_parts(Response::builder().header(header::CACHE_CONTROL, "max-age=60")), 281 | now, 282 | options, 283 | ); 284 | 285 | assert!(!policy.is_storable()); 286 | } 287 | 288 | #[test] 289 | fn test_request_max_age() { 290 | let now = SystemTime::now(); 291 | let first_request = request_parts(Request::builder()); 292 | let response = response_parts( 293 | Response::builder() 294 | .header(header::LAST_MODIFIED, format_date(-2, 3600)) 295 | .header(header::DATE, format_date(9, 60)) 296 | .header(header::AGE, 60) 297 | .header(header::EXPIRES, format_date(1, 3600)), 298 | ); 299 | 300 | let policy = CachePolicy::new_options( 301 | &first_request, 302 | &response, 303 | now, 304 | CacheOptions { 305 | shared: false, 306 | ..Default::default() 307 | }, 308 | ); 309 | 310 | assert_eq!(policy.age(now).as_secs(), 60); 311 | assert_eq!(policy.time_to_live(now).as_secs(), 3000); 312 | assert!(!policy.is_stale(now)); 313 | assert!(policy 314 | .before_request( 315 | &request_parts(Request::builder().header(header::CACHE_CONTROL, "max-age=90")), 316 | now 317 | ) 318 | .satisfies_without_revalidation()); 319 | assert!(!policy 320 | .before_request( 321 | &request_parts(Request::builder().header(header::CACHE_CONTROL, "max-age=30")), 322 | now 323 | ) 324 | .satisfies_without_revalidation()); 325 | } 326 | 327 | #[test] 328 | fn test_request_min_fresh() { 329 | let now = SystemTime::now(); 330 | let options = CacheOptions { 331 | shared: false, 332 | ..Default::default() 333 | }; 334 | 335 | let response = response_parts(Response::builder().header(header::CACHE_CONTROL, "max-age=60")); 336 | 337 | let policy = 338 | CachePolicy::new_options(&request_parts(Request::builder()), &response, now, options); 339 | 340 | assert!(!policy.is_stale(now)); 341 | 342 | assert!(!policy 343 | .before_request( 344 | &request_parts(Request::builder().header(header::CACHE_CONTROL, "min-fresh=120")), 345 | now 346 | ) 347 | .satisfies_without_revalidation()); 348 | 349 | assert!(policy 350 | .before_request( 351 | &request_parts(Request::builder().header(header::CACHE_CONTROL, "min-fresh=10")), 352 | now 353 | ) 354 | .satisfies_without_revalidation()); 355 | } 356 | 357 | #[test] 358 | fn test_request_max_stale() { 359 | let now = SystemTime::now(); 360 | let options = CacheOptions { 361 | shared: false, 362 | ..Default::default() 363 | }; 364 | 365 | let response = response_parts( 366 | Response::builder() 367 | .header(header::CACHE_CONTROL, "max-age=120") 368 | .header(header::AGE, 4 * 60), 369 | ); 370 | 371 | let policy = 372 | CachePolicy::new_options(&request_parts(Request::builder()), &response, now, options); 373 | 374 | assert!(policy.is_stale(now)); 375 | 376 | assert!(policy 377 | .before_request( 378 | &request_parts(Request::builder().header(header::CACHE_CONTROL, "max-stale=180")), 379 | now 380 | ) 381 | .satisfies_without_revalidation()); 382 | 383 | assert!(policy 384 | .before_request( 385 | &request_parts(Request::builder().header(header::CACHE_CONTROL, "max-stale")), 386 | now 387 | ) 388 | .satisfies_without_revalidation()); 389 | 390 | assert!(!policy 391 | .before_request( 392 | &request_parts(Request::builder().header(header::CACHE_CONTROL, "max-stale=10")), 393 | now 394 | ) 395 | .satisfies_without_revalidation()); 396 | } 397 | 398 | #[test] 399 | fn test_request_max_stale_not_honored_with_must_revalidate() { 400 | let now = SystemTime::now(); 401 | let options = CacheOptions { 402 | shared: false, 403 | ..Default::default() 404 | }; 405 | 406 | let response = response_parts( 407 | Response::builder() 408 | .header(header::CACHE_CONTROL, "max-age=120, must-revalidate") 409 | .header(header::DATE, format_date(15, 60)) 410 | .header(header::AGE, 4 * 60), 411 | ); 412 | 413 | let policy = 414 | CachePolicy::new_options(&request_parts(Request::builder()), &response, now, options); 415 | 416 | assert!(policy.is_stale(now)); 417 | 418 | assert!(!policy 419 | .before_request( 420 | &request_parts(Request::builder().header(header::CACHE_CONTROL, "max-stale=180")), 421 | now 422 | ) 423 | .satisfies_without_revalidation()); 424 | 425 | assert!(!policy 426 | .before_request( 427 | &request_parts(Request::builder().header(header::CACHE_CONTROL, "max-stale")), 428 | now 429 | ) 430 | .satisfies_without_revalidation()); 431 | } 432 | 433 | #[test] 434 | fn test_get_headers_deletes_cached_100_level_warnings() { 435 | let now = SystemTime::now(); 436 | let policy = CachePolicy::new( 437 | &request_parts(Request::builder().header("cache-control", "max-stale")), 438 | &response_parts( 439 | Response::builder() 440 | .header("cache-control", "immutable") 441 | .header(header::WARNING, "199 test danger, 200 ok ok"), 442 | ), 443 | ); 444 | 445 | assert_eq!( 446 | "200 ok ok", 447 | get_cached_response(&policy, &request_parts(Request::builder()), now).headers 448 | [header::WARNING.as_str()] 449 | ); 450 | } 451 | 452 | #[test] 453 | fn test_do_not_cache_partial_response() { 454 | let policy = CachePolicy::new( 455 | &request_parts(Request::builder()), 456 | &response_parts( 457 | Response::builder() 458 | .status(206) 459 | .header(header::CONTENT_RANGE, "bytes 100-100/200") 460 | .header(header::CACHE_CONTROL, "max-age=60"), 461 | ), 462 | ); 463 | 464 | assert!(!policy.is_storable()); 465 | } 466 | 467 | fn format_date(delta: i64, unit: i64) -> String { 468 | let now = OffsetDateTime::now_utc(); 469 | let timestamp = now.unix_timestamp() + delta * unit; 470 | 471 | let date = OffsetDateTime::from_unix_timestamp(timestamp).unwrap(); 472 | date.format(&Rfc2822).unwrap() 473 | } 474 | 475 | fn get_cached_response( 476 | policy: &CachePolicy, 477 | req: &impl http_cache_semantics::RequestLike, 478 | now: SystemTime, 479 | ) -> http::response::Parts { 480 | match policy.before_request(req, now) { 481 | http_cache_semantics::BeforeRequest::Fresh(res) => res, 482 | _ => panic!("stale"), 483 | } 484 | } 485 | -------------------------------------------------------------------------------- /tests/revalidate.rs: -------------------------------------------------------------------------------- 1 | use http::{header, HeaderMap, HeaderValue, Method, Request, Response}; 2 | use http_cache_semantics::CachePolicy; 3 | use std::time::Duration; 4 | use std::time::SystemTime; 5 | 6 | fn request_parts(builder: http::request::Builder) -> http::request::Parts { 7 | builder.body(()).unwrap().into_parts().0 8 | } 9 | 10 | fn response_parts(builder: http::response::Builder) -> http::response::Parts { 11 | builder.body(()).unwrap().into_parts().0 12 | } 13 | 14 | fn simple_request() -> http::request::Parts { 15 | request_parts(simple_request_builder()) 16 | } 17 | 18 | fn simple_request_builder() -> http::request::Builder { 19 | Request::builder() 20 | .method(Method::GET) 21 | .header(header::HOST, "www.w3c.org") 22 | .header(header::CONNECTION, "close") 23 | .header("x-custom", "yes") 24 | .uri("/Protocols/rfc2616/rfc2616-sec14.html") 25 | } 26 | 27 | fn cacheable_response_builder() -> http::response::Builder { 28 | Response::builder().header(header::CACHE_CONTROL, cacheable_header()) 29 | } 30 | 31 | fn simple_request_with_etagged_response() -> CachePolicy { 32 | CachePolicy::new( 33 | &simple_request(), 34 | &response_parts(cacheable_response_builder().header(header::ETAG, etag_value())), 35 | ) 36 | } 37 | 38 | fn simple_request_with_cacheable_response() -> CachePolicy { 39 | CachePolicy::new( 40 | &simple_request(), 41 | &response_parts(cacheable_response_builder()), 42 | ) 43 | } 44 | 45 | fn simple_request_with_always_variable_response() -> CachePolicy { 46 | CachePolicy::new( 47 | &simple_request(), 48 | &response_parts(cacheable_response_builder().header(header::VARY, "*")), 49 | ) 50 | } 51 | 52 | fn etag_value() -> &'static str { 53 | "\"123456789\"" 54 | } 55 | 56 | fn cacheable_header() -> &'static str { 57 | "max-age=111" 58 | } 59 | 60 | fn very_old_date() -> &'static str { 61 | "Tue, 15 Nov 1994 12:45:26 GMT" 62 | } 63 | 64 | fn assert_no_connection(headers: &HeaderMap) { 65 | assert!(!headers.contains_key(header::CONNECTION), "{headers:#?}"); 66 | } 67 | fn assert_custom_header(headers: &HeaderMap) { 68 | assert!(headers.contains_key("x-custom"), "{headers:#?}"); 69 | assert_eq!(headers.get("x-custom").unwrap(), "yes"); 70 | } 71 | 72 | fn assert_no_validators(headers: &HeaderMap) { 73 | assert!(!headers.contains_key(header::IF_NONE_MATCH)); 74 | assert!(!headers.contains_key(header::IF_MODIFIED_SINCE)); 75 | } 76 | 77 | #[test] 78 | fn test_ok_if_method_changes_to_head() { 79 | let now = SystemTime::now(); 80 | let policy = simple_request_with_etagged_response(); 81 | 82 | let headers = get_revalidation_request( 83 | &policy, 84 | &request_parts( 85 | simple_request_builder() 86 | .method(Method::HEAD) 87 | .header("pragma", "no-cache"), 88 | ), 89 | now, 90 | ) 91 | .headers; 92 | 93 | assert_custom_header(&headers); 94 | assert!(headers.contains_key(header::IF_NONE_MATCH), "{headers:#?}"); 95 | assert_eq!(headers.get(header::IF_NONE_MATCH).unwrap(), "\"123456789\""); 96 | } 97 | 98 | #[test] 99 | fn test_not_if_method_mismatch_other_than_head() { 100 | let now = SystemTime::now(); 101 | let policy = simple_request_with_etagged_response(); 102 | 103 | let incoming_request = request_parts(simple_request_builder().method(Method::POST)); 104 | let headers = get_revalidation_request( 105 | &policy, 106 | &incoming_request, 107 | now + Duration::from_secs(3600 * 24), 108 | ) 109 | .headers; 110 | 111 | assert_custom_header(&headers); 112 | assert_no_validators(&headers); 113 | } 114 | 115 | #[test] 116 | fn test_not_if_url_mismatch() { 117 | let now = SystemTime::now(); 118 | let policy = simple_request_with_etagged_response(); 119 | 120 | let incoming_request = request_parts(simple_request_builder().uri("/yomomma")); 121 | let headers = get_revalidation_request( 122 | &policy, 123 | &incoming_request, 124 | now + Duration::from_secs(3600 * 24), 125 | ) 126 | .headers; 127 | 128 | assert_custom_header(&headers); 129 | assert_no_validators(&headers); 130 | } 131 | 132 | #[test] 133 | fn test_not_if_host_mismatch() { 134 | let now = SystemTime::now(); 135 | let policy = simple_request_with_etagged_response(); 136 | 137 | let mut incoming_request = request_parts(simple_request_builder()); 138 | incoming_request 139 | .headers 140 | .insert(header::HOST, "www.w4c.org".parse().unwrap()); 141 | let headers = get_revalidation_request( 142 | &policy, 143 | dbg!(&incoming_request), 144 | now + Duration::from_secs(3600 * 24), 145 | ) 146 | .headers; 147 | 148 | assert_no_validators(&headers); 149 | assert!(headers.contains_key("x-custom")); 150 | } 151 | 152 | #[test] 153 | fn test_not_if_vary_fields_prevent() { 154 | let now = SystemTime::now(); 155 | let policy = simple_request_with_always_variable_response(); 156 | 157 | let headers = get_revalidation_request( 158 | &policy, 159 | &simple_request(), 160 | now + Duration::from_secs(3600 * 24), 161 | ) 162 | .headers; 163 | 164 | assert_custom_header(&headers); 165 | assert_no_validators(&headers); 166 | } 167 | 168 | #[test] 169 | fn test_when_entity_tag_validator_is_present() { 170 | let now = SystemTime::now(); 171 | let policy = simple_request_with_etagged_response(); 172 | 173 | let headers = get_revalidation_request( 174 | &policy, 175 | &simple_request(), 176 | now + Duration::from_secs(3600 * 24), 177 | ) 178 | .headers; 179 | 180 | assert_custom_header(&headers); 181 | assert_no_connection(&headers); 182 | assert_eq!(headers.get(header::IF_NONE_MATCH).unwrap(), "\"123456789\""); 183 | } 184 | 185 | #[test] 186 | fn test_skips_weak_validators_on_post() { 187 | let now = SystemTime::now(); 188 | let post_request = request_parts( 189 | simple_request_builder() 190 | .method(Method::POST) 191 | .header(header::IF_NONE_MATCH, "W/\"weak\", \"strong\", W/\"weak2\""), 192 | ); 193 | let policy = CachePolicy::new( 194 | &post_request, 195 | &response_parts( 196 | cacheable_response_builder() 197 | .header(header::LAST_MODIFIED, very_old_date()) 198 | .header(header::ETAG, etag_value()), 199 | ), 200 | ); 201 | 202 | let headers = get_revalidation_request( 203 | &policy, 204 | &post_request, 205 | now + Duration::from_secs(3600 * 24), 206 | ) 207 | .headers; 208 | 209 | assert_eq!( 210 | headers.get(header::IF_NONE_MATCH).unwrap(), 211 | "\"strong\", \"123456789\"" 212 | ); 213 | assert!(!headers.contains_key(header::IF_MODIFIED_SINCE)); 214 | } 215 | 216 | #[test] 217 | fn test_skips_weak_validators_on_post_2() { 218 | let now = SystemTime::now(); 219 | let post_request = request_parts( 220 | simple_request_builder() 221 | .method(Method::POST) 222 | .header(header::IF_NONE_MATCH, "W/\"weak\""), 223 | ); 224 | let policy = CachePolicy::new( 225 | &post_request, 226 | &response_parts( 227 | cacheable_response_builder().header(header::LAST_MODIFIED, very_old_date()), 228 | ), 229 | ); 230 | 231 | let headers = get_revalidation_request( 232 | &policy, 233 | &post_request, 234 | now + Duration::from_secs(3600 * 24), 235 | ) 236 | .headers; 237 | 238 | assert!(!headers.contains_key(header::IF_NONE_MATCH)); 239 | assert!(!headers.contains_key(header::IF_MODIFIED_SINCE)); 240 | } 241 | 242 | #[test] 243 | fn test_merges_validators() { 244 | let now = SystemTime::now(); 245 | let post_request = request_parts( 246 | simple_request_builder() 247 | .header(header::IF_NONE_MATCH, "W/\"weak\", \"strong\", W/\"weak2\""), 248 | ); 249 | let policy = CachePolicy::new( 250 | &post_request, 251 | &response_parts( 252 | cacheable_response_builder() 253 | .header(header::LAST_MODIFIED, very_old_date()) 254 | .header(header::ETAG, etag_value()) 255 | .header(header::CACHE_CONTROL, "must-revalidate"), 256 | ), 257 | ); 258 | 259 | let headers = get_revalidation_request( 260 | &policy, 261 | &post_request, 262 | now + Duration::from_secs(3600 * 24), 263 | ) 264 | .headers; 265 | 266 | assert_eq!( 267 | headers.get(header::IF_NONE_MATCH).unwrap(), 268 | "W/\"weak\", \"strong\", W/\"weak2\", \"123456789\"" 269 | ); 270 | assert_eq!( 271 | headers.get(header::IF_MODIFIED_SINCE).unwrap(), 272 | very_old_date() 273 | ); 274 | } 275 | 276 | #[test] 277 | fn test_when_last_modified_validator_is_present() { 278 | let now = SystemTime::now(); 279 | let policy = CachePolicy::new( 280 | &simple_request(), 281 | &response_parts( 282 | cacheable_response_builder().header(header::LAST_MODIFIED, very_old_date()), 283 | ), 284 | ); 285 | 286 | let headers = get_revalidation_request( 287 | &policy, 288 | &simple_request(), 289 | now + Duration::from_secs(3600 * 24), 290 | ) 291 | .headers; 292 | 293 | assert_custom_header(&headers); 294 | assert_no_connection(&headers); 295 | 296 | assert_eq!( 297 | headers.get(header::IF_MODIFIED_SINCE).unwrap(), 298 | very_old_date() 299 | ); 300 | let warn = headers.get(header::WARNING); 301 | assert!(warn.is_none() || !warn.unwrap().to_str().unwrap().contains("113")); 302 | } 303 | 304 | #[test] 305 | fn test_not_without_validators() { 306 | let now = SystemTime::now(); 307 | let policy = simple_request_with_cacheable_response(); 308 | let headers = get_revalidation_request( 309 | &policy, 310 | &simple_request(), 311 | now + Duration::from_secs(3600 * 24), 312 | ) 313 | .headers; 314 | 315 | assert_custom_header(&headers); 316 | assert_no_connection(&headers); 317 | assert_no_validators(&headers); 318 | 319 | let warn = headers.get(header::WARNING); 320 | 321 | assert!(warn.is_none() || !warn.unwrap().to_str().unwrap().contains("113")); 322 | } 323 | 324 | #[test] 325 | fn test_113_added() { 326 | let now = SystemTime::now(); 327 | let very_old_response = response_parts( 328 | Response::builder() 329 | .header(header::AGE, 3600 * 72) 330 | .header(header::LAST_MODIFIED, very_old_date()), 331 | ); 332 | let req = simple_request(); 333 | let policy = CachePolicy::new(&req, &very_old_response); 334 | 335 | let headers = get_cached_response(&policy, &req, now).headers; 336 | 337 | assert!(headers 338 | .get(header::WARNING) 339 | .unwrap() 340 | .to_str() 341 | .unwrap() 342 | .contains("113")); 343 | } 344 | 345 | #[test] 346 | fn test_removes_warnings() { 347 | let now = SystemTime::now(); 348 | let req = request_parts(Request::builder()); 349 | let policy = CachePolicy::new( 350 | &req, 351 | &response_parts( 352 | Response::builder() 353 | .header("cache-control", "max-age=2") 354 | .header(header::WARNING, "199 test danger"), 355 | ), 356 | ); 357 | 358 | assert!(!get_cached_response(&policy, &req, now) 359 | .headers 360 | .contains_key(header::WARNING)); 361 | } 362 | 363 | #[test] 364 | fn test_must_contain_any_etag() { 365 | let now = SystemTime::now(); 366 | let policy = CachePolicy::new( 367 | &simple_request(), 368 | &response_parts( 369 | cacheable_response_builder() 370 | .header(header::LAST_MODIFIED, very_old_date()) 371 | .header(header::ETAG, etag_value()), 372 | ), 373 | ); 374 | 375 | let headers = get_revalidation_request( 376 | &policy, 377 | &simple_request(), 378 | now + Duration::from_secs(3600 * 24), 379 | ) 380 | .headers; 381 | 382 | assert_eq!(headers.get(header::IF_NONE_MATCH).unwrap(), etag_value()); 383 | } 384 | 385 | #[test] 386 | fn test_merges_etags() { 387 | let now = SystemTime::now(); 388 | let policy = simple_request_with_etagged_response(); 389 | 390 | let headers = get_revalidation_request( 391 | &policy, 392 | &request_parts( 393 | simple_request_builder() 394 | .header(header::HOST, "www.w3c.org") 395 | .header(header::IF_NONE_MATCH, "\"foo\", \"bar\""), 396 | ), 397 | now + Duration::from_secs(3600 * 24), 398 | ) 399 | .headers; 400 | 401 | assert_eq!( 402 | headers.get(header::IF_NONE_MATCH).unwrap(), 403 | &format!("\"foo\", \"bar\", {}", etag_value())[..] 404 | ); 405 | } 406 | 407 | #[test] 408 | fn test_should_send_the_last_modified_value() { 409 | let now = SystemTime::now(); 410 | let policy = CachePolicy::new( 411 | &simple_request(), 412 | &response_parts( 413 | cacheable_response_builder() 414 | .header(header::LAST_MODIFIED, very_old_date()) 415 | .header(header::ETAG, etag_value()), 416 | ), 417 | ); 418 | 419 | let headers = get_revalidation_request( 420 | &policy, 421 | &simple_request(), 422 | now + Duration::from_secs(3600 * 24), 423 | ) 424 | .headers; 425 | 426 | assert_eq!( 427 | headers.get(header::IF_MODIFIED_SINCE).unwrap(), 428 | very_old_date() 429 | ); 430 | } 431 | 432 | #[test] 433 | fn test_should_not_send_the_last_modified_value_for_post() { 434 | let now = SystemTime::now(); 435 | let post_request = request_parts( 436 | Request::builder() 437 | .method(Method::POST) 438 | .header(header::IF_MODIFIED_SINCE, "yesterday"), 439 | ); 440 | 441 | let policy = CachePolicy::new( 442 | &post_request, 443 | &response_parts( 444 | cacheable_response_builder().header(header::LAST_MODIFIED, very_old_date()), 445 | ), 446 | ); 447 | 448 | let headers = get_revalidation_request( 449 | &policy, 450 | &post_request, 451 | now + Duration::from_secs(3600 * 24), 452 | ) 453 | .headers; 454 | 455 | assert!(!headers.contains_key(header::IF_MODIFIED_SINCE)); 456 | } 457 | 458 | #[test] 459 | fn test_should_not_send_the_last_modified_value_for_range_request() { 460 | let now = SystemTime::now(); 461 | let range_request = request_parts( 462 | Request::builder() 463 | .method(Method::GET) 464 | .header(header::ACCEPT_RANGES, "1-3") 465 | .header(header::IF_MODIFIED_SINCE, "yesterday"), 466 | ); 467 | 468 | let policy = CachePolicy::new( 469 | &range_request, 470 | &response_parts( 471 | cacheable_response_builder().header(header::LAST_MODIFIED, very_old_date()), 472 | ), 473 | ); 474 | 475 | let headers = get_revalidation_request( 476 | &policy, 477 | &range_request, 478 | now + Duration::from_secs(3600 * 24), 479 | ) 480 | .headers; 481 | 482 | assert!(!headers.contains_key(header::IF_MODIFIED_SINCE)); 483 | } 484 | 485 | fn get_cached_response( 486 | policy: &CachePolicy, 487 | req: &impl http_cache_semantics::RequestLike, 488 | now: SystemTime, 489 | ) -> http::response::Parts { 490 | match policy.before_request(req, now) { 491 | http_cache_semantics::BeforeRequest::Fresh(res) => res, 492 | _ => panic!("stale"), 493 | } 494 | } 495 | 496 | fn get_revalidation_request( 497 | policy: &CachePolicy, 498 | req: &(impl http_cache_semantics::RequestLike + std::fmt::Debug), 499 | now: SystemTime, 500 | ) -> http::request::Parts { 501 | match policy.before_request(req, now) { 502 | http_cache_semantics::BeforeRequest::Stale { request, matches } => { 503 | if !matches { 504 | eprintln!("warning: req doesn't match {req:#?} vs {policy:#?}"); 505 | } 506 | request 507 | } 508 | _ => panic!("no revalidation needed {req:#?} vs {policy:#?}"), 509 | } 510 | } 511 | -------------------------------------------------------------------------------- /tests/response.rs: -------------------------------------------------------------------------------- 1 | use http::{header, Method, Request, Response}; 2 | use http_cache_semantics::CacheOptions; 3 | use http_cache_semantics::CachePolicy; 4 | use std::time::SystemTime; 5 | use time::format_description::well_known::Rfc2822; 6 | use time::Duration; 7 | use time::OffsetDateTime; 8 | 9 | fn request_parts(builder: http::request::Builder) -> http::request::Parts { 10 | builder.body(()).unwrap().into_parts().0 11 | } 12 | 13 | fn response_parts(builder: http::response::Builder) -> http::response::Parts { 14 | builder.body(()).unwrap().into_parts().0 15 | } 16 | 17 | fn now_rfc2822() -> String { 18 | OffsetDateTime::now_utc().format(&Rfc2822).unwrap() 19 | } 20 | 21 | #[test] 22 | fn test_simple_miss() { 23 | let now = SystemTime::now(); 24 | let policy = CachePolicy::new( 25 | &request_parts(Request::builder().method(Method::GET)), 26 | &response_parts(Response::builder()), 27 | ); 28 | 29 | assert!(policy.is_stale(now)); 30 | } 31 | 32 | #[test] 33 | fn test_simple_hit() { 34 | let now = SystemTime::now(); 35 | let policy = CachePolicy::new( 36 | &request_parts(Request::builder().method(Method::GET)), 37 | &response_parts( 38 | Response::builder().header(header::CACHE_CONTROL, "public, max-age=999999"), 39 | ), 40 | ); 41 | 42 | assert!(!policy.is_stale(now)); 43 | assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 999999); 44 | } 45 | 46 | #[test] 47 | fn test_quoted_syntax() { 48 | let now = SystemTime::now(); 49 | let policy = CachePolicy::new( 50 | &request_parts(Request::builder().method(Method::GET)), 51 | &response_parts( 52 | Response::builder().header(header::CACHE_CONTROL, " max-age = \"678\" "), 53 | ), 54 | ); 55 | 56 | assert!(!policy.is_stale(now)); 57 | assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 678); 58 | } 59 | 60 | #[test] 61 | fn test_iis() { 62 | let now = SystemTime::now(); 63 | let policy = CachePolicy::new_options( 64 | &request_parts(Request::builder().method(Method::GET)), 65 | &response_parts( 66 | Response::builder().header(header::CACHE_CONTROL, "private, public, max-age=259200"), 67 | ), 68 | now, 69 | CacheOptions { 70 | shared: false, 71 | ..Default::default() 72 | }, 73 | ); 74 | 75 | assert!(!policy.is_stale(now)); 76 | assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 259200); 77 | } 78 | 79 | #[test] 80 | fn test_pre_check_tolerated() { 81 | let now = SystemTime::now(); 82 | let cache_control = "pre-check=0, post-check=0, no-store, no-cache, max-age=100"; 83 | 84 | let policy = CachePolicy::new( 85 | &request_parts(Request::builder().method(Method::GET)), 86 | &response_parts(Response::builder().header(header::CACHE_CONTROL, cache_control)), 87 | ); 88 | 89 | assert!(policy.is_stale(now)); 90 | assert!(!policy.is_storable()); 91 | assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 0); 92 | assert_eq!( 93 | get_cached_response( 94 | &policy, 95 | &request_parts(Request::builder().header("cache-control", "max-stale")), 96 | now 97 | ) 98 | .headers[header::CACHE_CONTROL.as_str()], 99 | cache_control 100 | ); 101 | } 102 | 103 | #[test] 104 | fn test_pre_check_poison() { 105 | let now = SystemTime::now(); 106 | let original_cache_control = 107 | "pre-check=0, post-check=0, no-cache, no-store, max-age=100, custom, foo=bar"; 108 | 109 | let policy = CachePolicy::new_options( 110 | &request_parts(Request::builder().method(Method::GET)), 111 | &response_parts( 112 | Response::builder() 113 | .header(header::CACHE_CONTROL, original_cache_control) 114 | .header(header::PRAGMA, "no-cache"), 115 | ), 116 | now, 117 | CacheOptions { 118 | ignore_cargo_cult: true, 119 | ..Default::default() 120 | }, 121 | ); 122 | 123 | assert!(!policy.is_stale(now)); 124 | assert!(policy.is_storable()); 125 | assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 100); 126 | 127 | let res = get_cached_response(&policy, &request_parts(Request::builder()), now); 128 | let cache_control_header = &res.headers[header::CACHE_CONTROL.as_str()]; 129 | assert!(!cache_control_header.to_str().unwrap().contains("pre-check")); 130 | assert!(!cache_control_header 131 | .to_str() 132 | .unwrap() 133 | .contains("post-check")); 134 | assert!(!cache_control_header.to_str().unwrap().contains("no-store")); 135 | 136 | assert!(cache_control_header 137 | .to_str() 138 | .unwrap() 139 | .contains("max-age=100")); 140 | assert!(cache_control_header.to_str().unwrap().contains("custom")); 141 | assert!(cache_control_header.to_str().unwrap().contains("foo=bar")); 142 | 143 | assert!(!res.headers.contains_key(header::PRAGMA.as_str())); 144 | } 145 | 146 | #[test] 147 | fn test_age_can_make_stale() { 148 | let now = SystemTime::now(); 149 | let policy = CachePolicy::new( 150 | &request_parts(Request::builder().method(Method::GET)), 151 | &response_parts( 152 | Response::builder() 153 | .header(header::CACHE_CONTROL, "max-age=100") 154 | .header(header::AGE, "101"), 155 | ), 156 | ); 157 | 158 | assert!(policy.is_stale(now)); 159 | assert!(policy.is_storable()); 160 | } 161 | 162 | #[test] 163 | fn test_age_not_always_stale() { 164 | let now = SystemTime::now(); 165 | let policy = CachePolicy::new( 166 | &request_parts(Request::builder().method(Method::GET)), 167 | &response_parts( 168 | Response::builder() 169 | .header(header::CACHE_CONTROL, "max-age=20") 170 | .header(header::AGE, "15"), 171 | ), 172 | ); 173 | 174 | assert!(!policy.is_stale(now)); 175 | assert!(policy.is_storable()); 176 | } 177 | 178 | #[test] 179 | fn test_bogus_age_ignored() { 180 | let now = SystemTime::now(); 181 | let policy = CachePolicy::new( 182 | &request_parts(Request::builder().method(Method::GET)), 183 | &response_parts( 184 | Response::builder() 185 | .header(header::CACHE_CONTROL, "max-age=20") 186 | .header(header::AGE, "golden"), 187 | ), 188 | ); 189 | 190 | assert!(!policy.is_stale(now)); 191 | assert!(policy.is_storable()); 192 | } 193 | 194 | #[test] 195 | fn test_cache_old_files() { 196 | let now = SystemTime::now(); 197 | let policy = CachePolicy::new( 198 | &request_parts(Request::builder().method(Method::GET)), 199 | &response_parts( 200 | Response::builder() 201 | .header(header::DATE, now_rfc2822()) 202 | .header(header::LAST_MODIFIED, "Mon, 07 Mar 2016 11:52:56 GMT"), 203 | ), 204 | ); 205 | 206 | assert!(!policy.is_stale(now)); 207 | assert!((policy.time_to_live(now) + policy.age(now)).as_secs() > 100); 208 | } 209 | 210 | #[test] 211 | fn test_immutable_simple_hit() { 212 | let now = SystemTime::now(); 213 | let policy = CachePolicy::new( 214 | &request_parts(Request::builder().method(Method::GET)), 215 | &response_parts( 216 | Response::builder().header(header::CACHE_CONTROL, "immutable, max-age=999999"), 217 | ), 218 | ); 219 | 220 | assert!(!policy.is_stale(now)); 221 | assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 999999); 222 | } 223 | 224 | #[test] 225 | fn test_immutable_can_expire() { 226 | let now = SystemTime::now(); 227 | let policy = CachePolicy::new( 228 | &request_parts(Request::builder().method(Method::GET)), 229 | &response_parts(Response::builder().header(header::CACHE_CONTROL, "immutable, max-age=0")), 230 | ); 231 | 232 | assert!(policy.is_stale(now)); 233 | assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 0); 234 | } 235 | 236 | #[test] 237 | fn test_cache_immutable_files() { 238 | let now = SystemTime::now(); 239 | let policy = CachePolicy::new( 240 | &request_parts(Request::builder().method(Method::GET)), 241 | &response_parts( 242 | Response::builder() 243 | .header(header::DATE, now_rfc2822()) 244 | .header(header::CACHE_CONTROL, "immutable") 245 | .header(header::LAST_MODIFIED, now_rfc2822()), 246 | ), 247 | ); 248 | 249 | assert!(!policy.is_stale(now)); 250 | assert!((policy.time_to_live(now) + policy.age(now)).as_secs() > 100); 251 | } 252 | 253 | #[test] 254 | fn test_immutable_can_be_off() { 255 | let now = SystemTime::now(); 256 | let policy = CachePolicy::new_options( 257 | &request_parts(Request::builder().method(Method::GET)), 258 | &response_parts( 259 | Response::builder() 260 | .header(header::DATE, now_rfc2822()) 261 | .header(header::CACHE_CONTROL, "immutable") 262 | .header(header::LAST_MODIFIED, now_rfc2822()), 263 | ), 264 | now, 265 | CacheOptions { 266 | immutable_min_time_to_live: std::time::Duration::from_secs(0), 267 | ..Default::default() 268 | }, 269 | ); 270 | 271 | assert!(policy.is_stale(now)); 272 | assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 0); 273 | } 274 | 275 | #[test] 276 | fn test_pragma_no_cache() { 277 | let now = SystemTime::now(); 278 | let policy = CachePolicy::new( 279 | &request_parts(Request::builder().method(Method::GET)), 280 | &response_parts( 281 | Response::builder() 282 | .header(header::PRAGMA, "no-cache") 283 | .header(header::LAST_MODIFIED, "Mon, 07 Mar 2016 11:52:56 GMT"), 284 | ), 285 | ); 286 | 287 | assert!(policy.is_stale(now)); 288 | } 289 | 290 | #[test] 291 | fn test_blank_cache_control_and_pragma_no_cache() { 292 | let now = SystemTime::now(); 293 | let policy = CachePolicy::new( 294 | &request_parts(Request::builder().method(Method::GET)), 295 | &response_parts( 296 | Response::builder() 297 | .header(header::CACHE_CONTROL, "") 298 | .header(header::PRAGMA, "no-cache") 299 | .header(header::LAST_MODIFIED, "Mon, 07 Mar 2016 11:52:56 GMT"), 300 | ), 301 | ); 302 | 303 | assert!(!policy.is_stale(now)); 304 | } 305 | 306 | #[test] 307 | fn test_no_store() { 308 | let now = SystemTime::now(); 309 | let policy = CachePolicy::new( 310 | &request_parts(Request::builder().method(Method::GET)), 311 | &response_parts( 312 | Response::builder().header(header::CACHE_CONTROL, "no-store, public, max-age=1"), 313 | ), 314 | ); 315 | 316 | assert!(policy.is_stale(now)); 317 | assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 0); 318 | } 319 | 320 | #[test] 321 | fn test_observe_private_cache() { 322 | let now = SystemTime::now(); 323 | let private_header = "private, max-age=1234"; 324 | 325 | let request = request_parts(Request::builder().method(Method::GET)); 326 | let response = 327 | response_parts(Response::builder().header(header::CACHE_CONTROL, private_header)); 328 | 329 | let shared_policy = CachePolicy::new(&request, &response); 330 | 331 | let unshared_policy = CachePolicy::new_options( 332 | &request, 333 | &response, 334 | now, 335 | CacheOptions { 336 | shared: false, 337 | ..Default::default() 338 | }, 339 | ); 340 | 341 | assert!(shared_policy.is_stale(now)); 342 | assert_eq!((shared_policy.time_to_live(now) + shared_policy.age(now)).as_secs(), 0); 343 | assert!(!unshared_policy.is_stale(now)); 344 | assert_eq!((unshared_policy.time_to_live(now) + unshared_policy.age(now)).as_secs(), 1234); 345 | } 346 | 347 | #[test] 348 | fn test_do_not_share_cookies() { 349 | let now = SystemTime::now(); 350 | let request = request_parts(Request::builder().method(Method::GET)); 351 | let response = response_parts( 352 | Response::builder() 353 | .header(header::SET_COOKIE, "foo=bar") 354 | .header(header::CACHE_CONTROL, "max-age=99"), 355 | ); 356 | 357 | let shared_policy = CachePolicy::new(&request, &response); 358 | 359 | let unshared_policy = CachePolicy::new_options( 360 | &request, 361 | &response, 362 | now, 363 | CacheOptions { 364 | shared: false, 365 | ..Default::default() 366 | }, 367 | ); 368 | 369 | assert!(shared_policy.is_stale(now)); 370 | assert_eq!((shared_policy.time_to_live(now) + shared_policy.age(now)).as_secs(), 0); 371 | assert!(!unshared_policy.is_stale(now)); 372 | assert_eq!((unshared_policy.time_to_live(now) + unshared_policy.age(now)).as_secs(), 99); 373 | } 374 | 375 | #[test] 376 | fn test_do_share_cookies_if_immutable() { 377 | let now = SystemTime::now(); 378 | let policy = CachePolicy::new( 379 | &request_parts(Request::builder().method(Method::GET)), 380 | &response_parts( 381 | Response::builder() 382 | .header(header::SET_COOKIE, "foo=bar") 383 | .header(header::CACHE_CONTROL, "immutable, max-age=99"), 384 | ), 385 | ); 386 | 387 | assert!(!policy.is_stale(now)); 388 | assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 99); 389 | } 390 | 391 | #[test] 392 | fn test_cache_explicitly_public_cookie() { 393 | let now = SystemTime::now(); 394 | let policy = CachePolicy::new( 395 | &request_parts(Request::builder().method(Method::GET)), 396 | &response_parts( 397 | Response::builder() 398 | .header(header::SET_COOKIE, "foo=bar") 399 | .header(header::CACHE_CONTROL, "max-age=5, public"), 400 | ), 401 | ); 402 | 403 | assert!(!policy.is_stale(now)); 404 | assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 5); 405 | } 406 | 407 | #[test] 408 | fn test_miss_max_age_equals_zero() { 409 | let now = SystemTime::now(); 410 | let policy = CachePolicy::new( 411 | &request_parts(Request::builder().method(Method::GET)), 412 | &response_parts(Response::builder().header(header::CACHE_CONTROL, "public, max-age=0")), 413 | ); 414 | 415 | assert!(policy.is_stale(now)); 416 | assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 0); 417 | } 418 | 419 | #[test] 420 | fn test_uncacheable_503() { 421 | let now = SystemTime::now(); 422 | let policy = CachePolicy::new( 423 | &request_parts(Request::builder().method(Method::GET)), 424 | &response_parts( 425 | Response::builder() 426 | .status(503) 427 | .header(header::CACHE_CONTROL, "public, max-age=0"), 428 | ), 429 | ); 430 | 431 | assert!(policy.is_stale(now)); 432 | assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 0); 433 | } 434 | 435 | #[test] 436 | fn test_cacheable_301() { 437 | let now = SystemTime::now(); 438 | let policy = CachePolicy::new( 439 | &request_parts(Request::builder().method(Method::GET)), 440 | &response_parts( 441 | Response::builder() 442 | .status(301) 443 | .header(header::LAST_MODIFIED, "Mon, 07 Mar 2016 11:52:56 GMT"), 444 | ), 445 | ); 446 | 447 | assert!(!policy.is_stale(now)); 448 | } 449 | 450 | #[test] 451 | fn test_uncacheable_303() { 452 | let now = SystemTime::now(); 453 | let policy = CachePolicy::new( 454 | &request_parts(Request::builder().method(Method::GET)), 455 | &response_parts( 456 | Response::builder() 457 | .status(303) 458 | .header(header::LAST_MODIFIED, "Mon, 07 Mar 2016 11:52:56 GMT"), 459 | ), 460 | ); 461 | 462 | assert!(policy.is_stale(now)); 463 | assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 0); 464 | } 465 | 466 | #[test] 467 | fn test_cacheable_303() { 468 | let now = SystemTime::now(); 469 | let policy = CachePolicy::new( 470 | &request_parts(Request::builder().method(Method::GET)), 471 | &response_parts( 472 | Response::builder() 473 | .status(303) 474 | .header(header::CACHE_CONTROL, "max-age=1000"), 475 | ), 476 | ); 477 | 478 | assert!(!policy.is_stale(now)); 479 | } 480 | 481 | #[test] 482 | fn test_uncacheable_412() { 483 | let now = SystemTime::now(); 484 | let policy = CachePolicy::new( 485 | &request_parts(Request::builder().method(Method::GET)), 486 | &response_parts( 487 | Response::builder() 488 | .status(412) 489 | .header(header::CACHE_CONTROL, "public, max-age=1000"), 490 | ), 491 | ); 492 | 493 | assert!(policy.is_stale(now)); 494 | assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 0); 495 | } 496 | 497 | #[test] 498 | fn test_expired_expires_cache_with_max_age() { 499 | let now = SystemTime::now(); 500 | let policy = CachePolicy::new( 501 | &request_parts(Request::builder().method(Method::GET)), 502 | &response_parts( 503 | Response::builder() 504 | .header(header::CACHE_CONTROL, "public, max-age=9999") 505 | .header(header::EXPIRES, "Sat, 07 May 2016 15:35:18 GMT"), 506 | ), 507 | ); 508 | 509 | assert!(!policy.is_stale(now)); 510 | assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 9999); 511 | } 512 | 513 | #[test] 514 | fn request_mismatches() { 515 | let now = SystemTime::now(); 516 | let policy = CachePolicy::new( 517 | &request_parts(Request::builder().method(Method::GET).uri("/test")), 518 | &response_parts( 519 | Response::builder() 520 | .header(header::CACHE_CONTROL, "public, max-age=9999") 521 | .header(header::EXPIRES, "Sat, 07 May 2016 15:35:18 GMT"), 522 | ), 523 | ); 524 | 525 | let mismatch = policy.before_request(&request_parts(Request::builder().method(Method::POST).uri("/test")), now); 526 | assert!(matches!(mismatch, http_cache_semantics::BeforeRequest::Stale {matches, ..} if !matches)); 527 | } 528 | 529 | #[test] 530 | fn request_matches() { 531 | let now = SystemTime::now(); 532 | let policy = CachePolicy::new( 533 | &request_parts(Request::builder().method(Method::GET).uri("/test")), 534 | &response_parts( 535 | Response::builder() 536 | .header(header::CACHE_CONTROL, "public, max-age=0") 537 | ), 538 | ); 539 | 540 | let mismatch = policy.before_request(&request_parts(Request::builder().method(Method::GET).uri("/test")), now); 541 | assert!(matches!(mismatch, http_cache_semantics::BeforeRequest::Stale {matches, ..} if matches)); 542 | } 543 | 544 | #[test] 545 | fn test_expired_expires_cached_with_s_maxage() { 546 | let now = SystemTime::now(); 547 | let request = request_parts(Request::builder().method(Method::GET)); 548 | let response = response_parts( 549 | Response::builder() 550 | .header(header::CACHE_CONTROL, "public, s-maxage=9999") 551 | .header(header::EXPIRES, "Sat, 07 May 2016 15:35:18 GMT"), 552 | ); 553 | 554 | let shared_policy = CachePolicy::new(&request, &response); 555 | 556 | let unshared_policy = CachePolicy::new_options( 557 | &request, 558 | &response, 559 | now, 560 | CacheOptions { 561 | shared: false, 562 | ..Default::default() 563 | }, 564 | ); 565 | 566 | assert!(!shared_policy.is_stale(now)); 567 | assert_eq!((shared_policy.time_to_live(now) + shared_policy.age(now)).as_secs(), 9999); 568 | assert!(unshared_policy.is_stale(now)); 569 | assert_eq!((unshared_policy.time_to_live(now) + unshared_policy.age(now)).as_secs(), 0); 570 | } 571 | 572 | #[test] 573 | fn test_max_age_wins_over_future_expires() { 574 | let now = SystemTime::now(); 575 | let policy = CachePolicy::new( 576 | &request_parts(Request::builder().method(Method::GET)), 577 | &response_parts( 578 | Response::builder() 579 | .header(header::CACHE_CONTROL, "public, max-age=333") 580 | .header( 581 | header::EXPIRES, 582 | OffsetDateTime::now_utc() 583 | .checked_add(Duration::hours(1)) 584 | .unwrap() 585 | .format(&Rfc2822) 586 | .unwrap(), 587 | ), 588 | ), 589 | ); 590 | 591 | assert!(!policy.is_stale(now)); 592 | assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 333); 593 | } 594 | 595 | fn get_cached_response( 596 | policy: &CachePolicy, 597 | req: &impl http_cache_semantics::RequestLike, 598 | now: SystemTime, 599 | ) -> http::response::Parts { 600 | match policy.before_request(req, now) { 601 | http_cache_semantics::BeforeRequest::Fresh(res) => res, 602 | _ => panic!("stale"), 603 | } 604 | } 605 | -------------------------------------------------------------------------------- /tests/responsetest.rs: -------------------------------------------------------------------------------- 1 | use http::*; 2 | use http_cache_semantics::*; 3 | use std::time::Duration; 4 | use std::time::SystemTime; 5 | use time::format_description::well_known::Rfc2822; 6 | use time::OffsetDateTime; 7 | 8 | macro_rules! headers( 9 | { $($key:tt : $value:expr),* $(,)? } => { 10 | { 11 | let mut m = Response::builder(); 12 | $( 13 | m = m.header($key, $value); 14 | )+ 15 | m.body(()).unwrap() 16 | } 17 | }; 18 | ); 19 | 20 | fn req() -> Request<()> { 21 | Request::get("http://test.example.com/").body(()).unwrap() 22 | } 23 | 24 | #[test] 25 | fn simple_miss() { 26 | let now = SystemTime::now(); 27 | let cache = CachePolicy::new(&req(), &Response::new(())); 28 | assert!(cache.is_stale(now)); 29 | } 30 | 31 | #[test] 32 | fn simple_hit() { 33 | let now = SystemTime::now(); 34 | let cache = CachePolicy::new( 35 | &req(), 36 | &headers! { "cache-control": "public, max-age=999999" }, 37 | ); 38 | assert!(!cache.is_stale(now)); 39 | assert_eq!(cache.time_to_live(now).as_secs(), 999999); 40 | } 41 | 42 | #[test] 43 | fn weird_syntax() { 44 | let now = SystemTime::now(); 45 | let cache = CachePolicy::new( 46 | &req(), 47 | &headers! { "cache-control": ",,,,max-age = 456 ," }, 48 | ); 49 | assert!(!cache.is_stale(now)); 50 | assert_eq!(cache.time_to_live(now).as_secs(), 456); 51 | 52 | // let cache2 = CachePolicy.fromObject( 53 | // JSON.parse(JSON.stringify(cache.toObject())) 54 | // ); 55 | // assert!(cache2 instanceof CachePolicy); 56 | // assert!(!cache2.is_stale(now)); 57 | // assert_eq!(cache2.max_age().as_secs(), 456); 58 | } 59 | 60 | #[test] 61 | fn quoted_syntax() { 62 | let now = SystemTime::now(); 63 | let cache = CachePolicy::new( 64 | &req(), 65 | &headers! { "cache-control": " max-age = \"678\" " }, 66 | ); 67 | assert!(!cache.is_stale(now)); 68 | assert_eq!(cache.time_to_live(now).as_secs(), 678); 69 | } 70 | 71 | #[test] 72 | fn iis() { 73 | let now = SystemTime::now(); 74 | let cache = CachePolicy::new_options( 75 | &req().into_parts().0, 76 | &headers! { 77 | "cache-control": "private, public, max-age=259200" 78 | } 79 | .into_parts() 80 | .0, 81 | now, 82 | CacheOptions { 83 | shared: false, 84 | ..Default::default() 85 | }, 86 | ); 87 | assert!(!cache.is_stale(now)); 88 | assert_eq!(cache.time_to_live(now).as_secs(), 259200); 89 | } 90 | 91 | #[test] 92 | fn pre_check_tolerated() { 93 | let now = SystemTime::now(); 94 | let cc = "pre-check=0, post-check=0, no-store, no-cache, max-age=100"; 95 | let cache = CachePolicy::new( 96 | &req(), 97 | &headers! { 98 | "cache-control": cc 99 | }, 100 | ); 101 | assert!(cache.is_stale(now), "{cache:#?}"); 102 | assert!(!cache.is_storable()); 103 | assert_eq!(cache.time_to_live(now).as_secs(), 0); 104 | assert_eq!( 105 | get_cached_response( 106 | &cache, 107 | &Request::get("http://test.example.com/") 108 | .header("cache-control", "max-stale") 109 | .body(()) 110 | .unwrap(), 111 | now 112 | ) 113 | .headers()["cache-control"], 114 | cc 115 | ); 116 | } 117 | 118 | #[test] 119 | fn pre_check_poison() { 120 | let now = SystemTime::now(); 121 | let orig_cc = "pre-check=0, post-check=0, no-cache, no-store, max-age=100, custom, foo=bar"; 122 | let res = &headers! { "cache-control": orig_cc, "pragma": "no-cache"}; 123 | let cache = CachePolicy::new_options( 124 | &req(), 125 | res, 126 | now, 127 | CacheOptions { 128 | ignore_cargo_cult: true, 129 | ..Default::default() 130 | }, 131 | ); 132 | assert!(!cache.is_stale(now)); 133 | assert!(cache.is_storable()); 134 | assert_eq!(cache.time_to_live(now).as_secs(), 100); 135 | 136 | let cc = get_cached_response(&cache, &req(), now); 137 | let cc = cc.headers(); 138 | let cc = cc["cache-control"].to_str().unwrap(); 139 | assert!(!cc.contains("pre-check")); 140 | assert!(!cc.contains("post-check")); 141 | assert!(!cc.contains("no-store")); 142 | 143 | assert!(cc.contains("max-age=100")); 144 | assert!(cc.contains(", custom") || cc.contains("custom, ")); 145 | assert!(cc.contains("foo=bar")); 146 | 147 | assert!(get_cached_response( 148 | &cache, 149 | &Request::get("http://test.example.com/") 150 | .header("cache-control", "max-stale") 151 | .body(()) 152 | .unwrap(), 153 | now 154 | ) 155 | .headers() 156 | .get("pragma") 157 | .is_none()); 158 | } 159 | 160 | #[test] 161 | fn pre_check_poison_undefined_header() { 162 | let now = SystemTime::now(); 163 | let orig_cc = "pre-check=0, post-check=0, no-cache, no-store"; 164 | let res = &headers! { "cache-control": orig_cc, "expires": "yesterday!"}; 165 | let cache = CachePolicy::new_options( 166 | &req(), 167 | res, 168 | now, 169 | CacheOptions { 170 | ignore_cargo_cult: true, 171 | ..Default::default() 172 | }, 173 | ); 174 | assert!(cache.is_stale(now)); 175 | assert!(cache.is_storable()); 176 | assert_eq!(cache.time_to_live(now).as_secs(), 0); 177 | 178 | let res = &get_cached_response( 179 | &cache, 180 | &Request::get("http://test.example.com/") 181 | .header("cache-control", "max-stale") 182 | .body(()) 183 | .unwrap(), 184 | now, 185 | ); 186 | let _cc = &res.headers()["cache-control"]; 187 | 188 | assert!(res.headers().get("expires").is_none()); 189 | } 190 | 191 | #[test] 192 | fn cache_with_expires() { 193 | let now = SystemTime::now(); 194 | let cache = CachePolicy::new( 195 | &req(), 196 | &headers! { 197 | "date": date_str(now), 198 | "expires": date_str(now + Duration::from_millis(2001)), 199 | }, 200 | ); 201 | assert!(!cache.is_stale(now)); 202 | assert_eq!(2, cache.time_to_live(now).as_secs()); 203 | } 204 | 205 | #[test] 206 | fn cache_with_expires_relative_to_date() { 207 | let now = SystemTime::now(); 208 | let cache = CachePolicy::new( 209 | &req(), 210 | &headers! { 211 | "date": date_str(now - Duration::from_secs(30)), 212 | "expires": date_str(now), 213 | }, 214 | ); 215 | assert_eq!(30, cache.time_to_live(now).as_secs()); 216 | } 217 | 218 | #[test] 219 | fn cache_with_expires_always_relative_to_date() { 220 | let now = SystemTime::now(); 221 | let cache = CachePolicy::new_options( 222 | &req(), 223 | &headers! { 224 | "date": date_str(now - Duration::from_secs(3)), 225 | "expires": date_str(now), 226 | }, 227 | now, 228 | Default::default(), 229 | ); 230 | assert_eq!(3, cache.time_to_live(now).as_secs()); 231 | } 232 | 233 | #[test] 234 | fn cache_expires_no_date() { 235 | let now = SystemTime::now(); 236 | let cache = CachePolicy::new( 237 | &req(), 238 | &headers! { 239 | "cache-control": "public", 240 | "expires": date_str(now + Duration::from_secs(3600)), 241 | }, 242 | ); 243 | assert!(!cache.is_stale(now)); 244 | assert!(cache.time_to_live(now).as_secs() > 3595); 245 | assert!(cache.time_to_live(now).as_secs() < 3605); 246 | } 247 | 248 | #[test] 249 | fn ages() { 250 | let mut now = SystemTime::now(); 251 | let cache = CachePolicy::new( 252 | &req(), 253 | &headers! { 254 | "cache-control": "max-age=100", 255 | "age": "50", 256 | }, 257 | ); 258 | assert!(cache.is_storable()); 259 | 260 | assert_eq!(50, cache.time_to_live(now).as_secs()); 261 | assert!(!cache.is_stale(now)); 262 | now += Duration::from_secs(48); 263 | assert_eq!(2, cache.time_to_live(now).as_secs()); 264 | assert!(!cache.is_stale(now)); 265 | now += Duration::from_secs(5); 266 | assert!(cache.is_stale(now)); 267 | assert_eq!(0, cache.time_to_live(now).as_secs()); 268 | } 269 | 270 | #[test] 271 | fn age_can_make_stale() { 272 | let now = SystemTime::now(); 273 | let cache = CachePolicy::new( 274 | &req(), 275 | &headers! { 276 | "cache-control": "max-age=100", 277 | "age": "101", 278 | }, 279 | ); 280 | assert!(cache.is_stale(now)); 281 | assert!(cache.is_storable()); 282 | } 283 | 284 | #[test] 285 | fn age_not_always_stale() { 286 | let now = SystemTime::now(); 287 | let cache = CachePolicy::new( 288 | &req(), 289 | &headers! { 290 | "cache-control": "max-age=20", 291 | "age": "15", 292 | }, 293 | ); 294 | assert!(!cache.is_stale(now)); 295 | assert!(cache.is_storable()); 296 | } 297 | 298 | #[test] 299 | fn bogus_age_ignored() { 300 | let now = SystemTime::now(); 301 | let cache = CachePolicy::new( 302 | &req(), 303 | &headers! { 304 | "cache-control": "max-age=20", 305 | "age": "golden", 306 | }, 307 | ); 308 | assert!(!cache.is_stale(now)); 309 | assert!(cache.is_storable()); 310 | } 311 | 312 | #[test] 313 | fn cache_old_files() { 314 | let now = SystemTime::now(); 315 | let cache = CachePolicy::new( 316 | &req(), 317 | &headers! { 318 | "date": date_str(now), 319 | "last-modified": "Mon, 07 Mar 2016 11:52:56 GMT", 320 | }, 321 | ); 322 | assert!(!cache.is_stale(now)); 323 | assert!(cache.time_to_live(now).as_secs() > 100); 324 | } 325 | 326 | #[test] 327 | fn immutable_simple_hit() { 328 | let now = SystemTime::now(); 329 | let cache = CachePolicy::new( 330 | &req(), 331 | &headers! { "cache-control": "immutable, max-age=999999" }, 332 | ); 333 | assert!(!cache.is_stale(now)); 334 | assert_eq!(cache.time_to_live(now).as_secs(), 999999); 335 | } 336 | 337 | #[test] 338 | fn immutable_can_expire() { 339 | let now = SystemTime::now(); 340 | let cache = CachePolicy::new( 341 | &req(), 342 | &headers! { 343 | "cache-control": "immutable, max-age=0" 344 | }, 345 | ); 346 | assert!(cache.is_stale(now)); 347 | assert_eq!(cache.time_to_live(now).as_secs(), 0); 348 | } 349 | 350 | #[test] 351 | fn cache_immutable_files() { 352 | let now = SystemTime::now(); 353 | let cache = CachePolicy::new( 354 | &req(), 355 | &headers! { 356 | "date": date_str(now), 357 | "cache-control": "immutable", 358 | "last-modified": date_str(now), 359 | }, 360 | ); 361 | assert!(!cache.is_stale(now)); 362 | assert!(cache.time_to_live(now).as_secs() > 100); 363 | } 364 | 365 | #[test] 366 | fn immutable_can_be_off() { 367 | let now = SystemTime::now(); 368 | let cache = CachePolicy::new_options( 369 | &req(), 370 | &headers! { 371 | "date": date_str(now), 372 | "cache-control": "immutable", 373 | "last-modified": date_str(now), 374 | }, 375 | now, 376 | CacheOptions { 377 | immutable_min_time_to_live: Duration::from_secs(0), 378 | ..Default::default() 379 | }, 380 | ); 381 | assert!(cache.is_stale(now)); 382 | assert_eq!(cache.time_to_live(now).as_secs(), 0); 383 | } 384 | 385 | #[test] 386 | fn pragma_no_cache() { 387 | let now = SystemTime::now(); 388 | let cache = CachePolicy::new( 389 | &req(), 390 | &headers! { 391 | "pragma": "no-cache", 392 | "last-modified": "Mon, 07 Mar 2016 11:52:56 GMT", 393 | }, 394 | ); 395 | assert!(cache.is_stale(now)); 396 | } 397 | 398 | #[test] 399 | fn blank_cache_control_and_pragma_no_cache() { 400 | let cache = CachePolicy::new( 401 | &req(), 402 | &headers! { 403 | "cache-control": "", 404 | "pragma": "no-cache", 405 | "last-modified": date_str(SystemTime::now() - Duration::from_secs(10)), 406 | }, 407 | ); 408 | assert!(!cache.is_stale(SystemTime::now()), "{cache:#?}"); 409 | } 410 | 411 | #[test] 412 | fn no_store() { 413 | let now = SystemTime::now(); 414 | let cache = CachePolicy::new( 415 | &req(), 416 | &headers! { "cache-control": "no-store, public, max-age=1", }, 417 | ); 418 | assert!(cache.is_stale(now)); 419 | assert_eq!(0, cache.time_to_live(now).as_secs()); 420 | } 421 | 422 | #[test] 423 | fn observe_private_cache() { 424 | let now = SystemTime::now(); 425 | let proxy_cache = CachePolicy::new( 426 | &req(), 427 | &headers! { 428 | "cache-control": "private, max-age=1234", 429 | }, 430 | ); 431 | assert!(proxy_cache.is_stale(now)); 432 | assert_eq!(0, proxy_cache.time_to_live(now).as_secs()); 433 | 434 | let ua_cache = CachePolicy::new_options( 435 | &req(), 436 | &headers! { 437 | "cache-control": "private, max-age=1234", 438 | }, 439 | now, 440 | CacheOptions { 441 | shared: false, 442 | ..Default::default() 443 | }, 444 | ); 445 | assert!(!ua_cache.is_stale(now)); 446 | assert_eq!(1234, ua_cache.time_to_live(now).as_secs()); 447 | } 448 | 449 | #[test] 450 | fn don_t_share_cookies() { 451 | let now = SystemTime::now(); 452 | let proxy_cache = CachePolicy::new_options( 453 | &req(), 454 | &headers! { 455 | "set-cookie": "foo=bar", 456 | "cache-control": "max-age=99", 457 | }, 458 | now, 459 | CacheOptions { 460 | shared: true, 461 | ..Default::default() 462 | }, 463 | ); 464 | assert!(proxy_cache.is_stale(now)); 465 | assert_eq!(0, proxy_cache.time_to_live(now).as_secs()); 466 | 467 | let ua_cache = CachePolicy::new_options( 468 | &req(), 469 | &headers! { 470 | "set-cookie": "foo=bar", 471 | "cache-control": "max-age=99", 472 | }, 473 | now, 474 | CacheOptions { 475 | shared: false, 476 | ..Default::default() 477 | }, 478 | ); 479 | assert!(!ua_cache.is_stale(now)); 480 | assert_eq!(99, ua_cache.time_to_live(now).as_secs()); 481 | } 482 | 483 | #[test] 484 | fn do_share_cookies_if_immutable() { 485 | let now = SystemTime::now(); 486 | let proxy_cache = CachePolicy::new_options( 487 | &req(), 488 | &headers! { 489 | "set-cookie": "foo=bar", 490 | "cache-control": "immutable, max-age=99", 491 | }, 492 | now, 493 | CacheOptions { 494 | shared: true, 495 | ..Default::default() 496 | }, 497 | ); 498 | assert!(!proxy_cache.is_stale(now)); 499 | assert_eq!(99, proxy_cache.time_to_live(now).as_secs()); 500 | } 501 | 502 | #[test] 503 | fn cache_explicitly_public_cookie() { 504 | let now = SystemTime::now(); 505 | let proxy_cache = CachePolicy::new_options( 506 | &req(), 507 | &headers! { 508 | "set-cookie": "foo=bar", 509 | "cache-control": "max-age=5, public", 510 | }, 511 | now, 512 | CacheOptions { 513 | shared: true, 514 | ..Default::default() 515 | }, 516 | ); 517 | assert!(!proxy_cache.is_stale(now)); 518 | assert_eq!(5, proxy_cache.time_to_live(now).as_secs()); 519 | } 520 | 521 | #[test] 522 | fn miss_max_age_0() { 523 | let now = SystemTime::now(); 524 | let cache = CachePolicy::new( 525 | &req(), 526 | &headers! { "cache-control": "public, max-age=0", }, 527 | ); 528 | assert!(cache.is_stale(now)); 529 | assert_eq!(0, cache.time_to_live(now).as_secs()); 530 | } 531 | 532 | #[test] 533 | fn uncacheable_503() { 534 | let now = SystemTime::now(); 535 | let mut res = headers! { "cache-control": "public, max-age=1000" }; 536 | *res.status_mut() = StatusCode::from_u16(503).unwrap(); 537 | let cache = CachePolicy::new(&req(), &res); 538 | assert!(cache.is_stale(now)); 539 | assert_eq!(0, cache.time_to_live(now).as_secs()); 540 | } 541 | 542 | #[test] 543 | fn cacheable_301() { 544 | let now = SystemTime::now(); 545 | let mut res = headers! { "last-modified": "Mon, 07 Mar 2016 11:52:56 GMT", }; 546 | *res.status_mut() = StatusCode::from_u16(301).unwrap(); 547 | let cache = CachePolicy::new(&req(), &res); 548 | assert!(!cache.is_stale(now)); 549 | } 550 | 551 | #[test] 552 | fn uncacheable_303() { 553 | let now = SystemTime::now(); 554 | let mut res = headers! { "last-modified": "Mon, 07 Mar 2016 11:52:56 GMT", }; 555 | *res.status_mut() = StatusCode::from_u16(303).unwrap(); 556 | let cache = CachePolicy::new(&req(), &res); 557 | assert!(cache.is_stale(now)); 558 | assert_eq!(0, cache.time_to_live(now).as_secs()); 559 | } 560 | 561 | #[test] 562 | fn cacheable_303() { 563 | let now = SystemTime::now(); 564 | let mut res = headers! { "cache-control": "max-age=1000", }; 565 | *res.status_mut() = StatusCode::from_u16(303).unwrap(); 566 | let cache = CachePolicy::new(&req(), &res); 567 | assert!(!cache.is_stale(now)); 568 | } 569 | 570 | #[test] 571 | fn uncacheable_412() { 572 | let now = SystemTime::now(); 573 | let mut res = headers! { "cache-control": "public, max-age=1000", }; 574 | *res.status_mut() = StatusCode::from_u16(412).unwrap(); 575 | let cache = CachePolicy::new(&req(), &res); 576 | assert!(cache.is_stale(now)); 577 | assert_eq!(0, cache.time_to_live(now).as_secs()); 578 | } 579 | 580 | #[test] 581 | fn expired_expires_cached_with_max_age() { 582 | let now = SystemTime::now(); 583 | let cache = CachePolicy::new( 584 | &req(), 585 | &headers! { 586 | "cache-control": "public, max-age=9999", 587 | "expires": "Sat, 07 May 2016 15:35:18 GMT", 588 | }, 589 | ); 590 | assert!(!cache.is_stale(now)); 591 | assert_eq!(9999, cache.time_to_live(now).as_secs()); 592 | } 593 | 594 | #[test] 595 | fn expired_expires_cached_with_s_maxage() { 596 | let now = SystemTime::now(); 597 | let proxy_cache = CachePolicy::new( 598 | &req(), 599 | &headers! { 600 | "cache-control": "public, s-maxage=9999", 601 | "expires": "Sat, 07 May 2016 15:35:18 GMT", 602 | }, 603 | ); 604 | assert!(!proxy_cache.is_stale(now)); 605 | assert_eq!(9999, proxy_cache.time_to_live(now).as_secs()); 606 | 607 | let ua_cache = CachePolicy::new_options( 608 | &req(), 609 | &headers! { 610 | "cache-control": "public, s-maxage=9999", 611 | "expires": "Sat, 07 May 2016 15:35:18 GMT", 612 | }, 613 | now, 614 | CacheOptions { 615 | shared: false, 616 | ..Default::default() 617 | }, 618 | ); 619 | assert!(ua_cache.is_stale(now)); 620 | assert_eq!(0, ua_cache.time_to_live(now).as_secs()); 621 | } 622 | 623 | #[test] 624 | fn max_age_wins_over_future_expires() { 625 | let now = SystemTime::now(); 626 | let cache = CachePolicy::new( 627 | &req(), 628 | &headers! { 629 | "cache-control": "public, max-age=333", 630 | "expires": date_str(now + Duration::from_secs(3600)), 631 | }, 632 | ); 633 | assert!(!cache.is_stale(now)); 634 | assert_eq!(333, cache.time_to_live(now).as_secs()); 635 | } 636 | 637 | #[test] 638 | fn remove_hop_headers() { 639 | let mut now = SystemTime::now(); 640 | let res = &headers! { 641 | "te": "deflate", 642 | "date": "now", 643 | "custom": "header", 644 | "oompa": "lumpa", 645 | "connection": "close, oompa, header", 646 | "age": "10", 647 | "cache-control": "public, max-age=333", 648 | }; 649 | let cache = CachePolicy::new(&req(), res); 650 | 651 | now += Duration::from_millis(1005); 652 | let h = get_cached_response(&cache, &req(), now); 653 | let h = h.headers(); 654 | assert!(h.get("connection").is_none()); 655 | assert!(h.get("te").is_none()); 656 | assert!(h.get("oompa").is_none()); 657 | assert_eq!(h["cache-control"].to_str().unwrap(), "public, max-age=333"); 658 | assert_ne!( 659 | h["date"].to_str().unwrap(), 660 | "now", 661 | "updated age requires updated date" 662 | ); 663 | assert_eq!(h["custom"].to_str().unwrap(), "header"); 664 | assert_eq!(h["age"].to_str().unwrap(), "11"); 665 | 666 | // let cache2 = TimeTravellingPolicy.fromObject( 667 | // JSON.parse(JSON.stringify(cache.toObject())) 668 | // ); 669 | // assert!(cache2 instanceof TimeTravellingPolicy); 670 | // let h2 = cache2.cached_response(now).headers(); 671 | // assert.deepEqual(h, h2); 672 | } 673 | 674 | fn date_str(now: SystemTime) -> String { 675 | let timestamp = now 676 | .duration_since(SystemTime::UNIX_EPOCH) 677 | .unwrap() 678 | .as_secs(); 679 | let date = OffsetDateTime::from_unix_timestamp(timestamp as i64).unwrap(); 680 | date.format(&Rfc2822).unwrap() 681 | } 682 | 683 | fn get_cached_response( 684 | policy: &CachePolicy, 685 | req: &impl http_cache_semantics::RequestLike, 686 | now: SystemTime, 687 | ) -> http::response::Parts { 688 | match policy.before_request(req, now) { 689 | http_cache_semantics::BeforeRequest::Fresh(res) => res, 690 | _ => panic!("stale"), 691 | } 692 | } 693 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(missing_docs)] 2 | #![deny(unconditional_recursion)] 3 | //! Tells when responses can be reused from a cache, taking into account [HTTP RFC 7234](http://httpwg.org/specs/rfc7234.html) rules for user agents and shared caches. 4 | //! It's aware of many tricky details such as the `Vary` header, proxy revalidation, and authenticated responses. 5 | 6 | use http::HeaderMap; 7 | use http::HeaderValue; 8 | use http::Method; 9 | use http::Request; 10 | use http::Response; 11 | use http::StatusCode; 12 | use http::Uri; 13 | use std::collections::hash_map::Entry; 14 | use std::collections::HashMap; 15 | use std::time::Duration; 16 | use std::time::SystemTime; 17 | use time::format_description::well_known::Rfc2822; 18 | use time::OffsetDateTime; 19 | 20 | // rfc7231 6.1 21 | const STATUS_CODE_CACHEABLE_BY_DEFAULT: &[u16] = 22 | &[200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501]; 23 | 24 | // This implementation does not understand partial responses (206) 25 | const UNDERSTOOD_STATUSES: &[u16] = &[ 26 | 200, 203, 204, 300, 301, 302, 303, 307, 308, 404, 405, 410, 414, 501, 27 | ]; 28 | 29 | const HOP_BY_HOP_HEADERS: &[&str] = &[ 30 | "date", // included, because we add Age update Date 31 | "connection", 32 | "keep-alive", 33 | "proxy-authenticate", 34 | "proxy-authorization", 35 | "te", 36 | "trailer", 37 | "transfer-encoding", 38 | "upgrade", 39 | ]; 40 | 41 | const EXCLUDED_FROM_REVALIDATION_UPDATE: &[&str] = &[ 42 | // Since the old body is reused, it doesn't make sense to change properties of the body 43 | "content-length", 44 | "content-encoding", 45 | "transfer-encoding", 46 | "content-range", 47 | ]; 48 | 49 | type CacheControl = HashMap, Option>>; 50 | 51 | fn parse_cache_control<'a>(headers: impl IntoIterator) -> CacheControl { 52 | let mut cc = CacheControl::new(); 53 | let mut is_valid = true; 54 | 55 | for h in headers.into_iter().filter_map(|v| v.to_str().ok()) { 56 | for part in h.split(',') { 57 | // TODO: lame parsing 58 | if part.trim().is_empty() { 59 | continue; 60 | } 61 | let mut kv = part.splitn(2, '='); 62 | let k = kv.next().unwrap().trim(); 63 | if k.is_empty() { 64 | continue; 65 | } 66 | let v = kv.next().map(str::trim); 67 | match cc.entry(k.into()) { 68 | Entry::Occupied(e) => { 69 | // When there is more than one value present for a given directive (e.g., two Expires header fields, multiple Cache-Control: max-age directives), 70 | // the directive's value is considered invalid. Caches are encouraged to consider responses that have invalid freshness information to be stale 71 | if e.get().as_deref() != v { 72 | is_valid = false; 73 | } 74 | } 75 | Entry::Vacant(e) => { 76 | e.insert(v.map(|v| v.trim_matches('"')).map(From::from)); // TODO: bad unquoting 77 | } 78 | } 79 | } 80 | } 81 | if !is_valid { 82 | cc.insert("must-revalidate".into(), None); 83 | } 84 | cc 85 | } 86 | 87 | fn format_cache_control(cc: &CacheControl) -> String { 88 | let mut out = String::new(); 89 | for (k, v) in cc { 90 | if !out.is_empty() { 91 | out.push_str(", "); 92 | } 93 | out.push_str(k); 94 | if let Some(v) = v { 95 | out.push('='); 96 | let needs_quote = 97 | v.is_empty() || v.as_bytes().iter().any(|b| !b.is_ascii_alphanumeric()); 98 | if needs_quote { 99 | out.push('"'); 100 | } 101 | out.push_str(v); 102 | if needs_quote { 103 | out.push('"'); 104 | } 105 | } 106 | } 107 | out 108 | } 109 | 110 | /// Configuration options which control behavior of the cache. Use with `CachePolicy::new_options()`. 111 | #[derive(Debug, Copy, Clone)] 112 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 113 | pub struct CacheOptions { 114 | /// If `true` (default), then the response is evaluated from a 115 | /// perspective of a shared cache (i.e. `private` is not cacheable and 116 | /// `s-maxage` is respected). If `shared` is `false`, then the response is 117 | /// evaluated from a perspective of a single-user cache (i.e. `private` is 118 | /// cacheable and `s-maxage` is ignored). `shared: true` is required 119 | /// for proxies and multi-user caches. 120 | pub shared: bool, 121 | /// `cache_heuristic` is a fraction of response's age that is used as a 122 | /// fallback cache duration. The default is 0.1 (10%), e.g. if a file 123 | /// hasn't been modified for 100 days, it'll be cached for 100×0.1 = 10 124 | /// days. 125 | pub cache_heuristic: f32, 126 | /// `immutable_min_time_to_live` is a duration to assume as the 127 | /// default time to cache responses with `Cache-Control: immutable`. Note 128 | /// that per RFC these can become stale, so `max-age` still overrides the 129 | /// default. 130 | pub immutable_min_time_to_live: Duration, 131 | /// If `ignore_cargo_cult` is `true`, common anti-cache directives will be 132 | /// completely ignored if the non-standard `pre-check` and `post-check` 133 | /// directives are present. These two useless directives are most commonly 134 | /// found in bad StackOverflow answers and PHP's "session limiter" 135 | /// defaults. 136 | pub ignore_cargo_cult: bool, 137 | } 138 | 139 | impl Default for CacheOptions { 140 | fn default() -> Self { 141 | Self { 142 | shared: true, 143 | cache_heuristic: 0.1, // 10% matches IE 144 | immutable_min_time_to_live: Duration::from_secs(24 * 3600), 145 | ignore_cargo_cult: false, 146 | } 147 | } 148 | } 149 | 150 | /// Identifies when responses can be reused from a cache, taking into account 151 | /// HTTP RFC 7234 rules for user agents and shared caches. It's aware of many 152 | /// tricky details such as the Vary header, proxy revalidation, and 153 | /// authenticated responses. 154 | #[derive(Debug, Clone)] 155 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 156 | pub struct CachePolicy { 157 | #[cfg_attr(feature = "serde", serde(with = "http_serde::header_map"))] 158 | req: HeaderMap, 159 | #[cfg_attr(feature = "serde", serde(with = "http_serde::header_map"))] 160 | res: HeaderMap, 161 | #[cfg_attr(feature = "serde", serde(with = "http_serde::uri"))] 162 | uri: Uri, 163 | #[cfg_attr(feature = "serde", serde(with = "http_serde::status_code"))] 164 | status: StatusCode, 165 | #[cfg_attr(feature = "serde", serde(with = "http_serde::method"))] 166 | method: Method, 167 | opts: CacheOptions, 168 | res_cc: CacheControl, 169 | req_cc: CacheControl, 170 | response_time: SystemTime, 171 | } 172 | 173 | impl CachePolicy { 174 | /// Cacheability of an HTTP response depends on how it was requested, so 175 | /// both request and response are required to create the policy. 176 | #[inline] 177 | pub fn new(req: &Req, res: &Res) -> Self { 178 | let uri = req.uri(); 179 | let status = res.status(); 180 | let method = req.method().clone(); 181 | let res = res.headers().clone(); 182 | let req = req.headers().clone(); 183 | Self::from_details( 184 | uri, 185 | method, 186 | status, 187 | req, 188 | res, 189 | SystemTime::now(), 190 | Default::default(), 191 | ) 192 | } 193 | 194 | /// Caching with customized behavior. See `CacheOptions` for details. 195 | /// 196 | /// `response_time` is a timestamp when the response has been received, usually `SystemTime::now()`. 197 | #[inline] 198 | pub fn new_options( 199 | req: &Req, 200 | res: &Res, 201 | response_time: SystemTime, 202 | opts: CacheOptions, 203 | ) -> Self { 204 | let uri = req.uri(); 205 | let status = res.status(); 206 | let method = req.method().clone(); 207 | let res = res.headers().clone(); 208 | let req = req.headers().clone(); 209 | Self::from_details(uri, method, status, req, res, response_time, opts) 210 | } 211 | 212 | fn from_details( 213 | uri: Uri, 214 | method: Method, 215 | status: StatusCode, 216 | req: HeaderMap, 217 | mut res: HeaderMap, 218 | response_time: SystemTime, 219 | opts: CacheOptions, 220 | ) -> Self { 221 | let mut res_cc = parse_cache_control(res.get_all("cache-control")); 222 | let req_cc = parse_cache_control(req.get_all("cache-control")); 223 | 224 | // Assume that if someone uses legacy, non-standard uncecessary options they don't understand caching, 225 | // so there's no point stricly adhering to the blindly copy&pasted directives. 226 | if opts.ignore_cargo_cult 227 | && res_cc.get("pre-check").is_some() 228 | && res_cc.get("post-check").is_some() 229 | { 230 | res_cc.remove("pre-check"); 231 | res_cc.remove("post-check"); 232 | res_cc.remove("no-cache"); 233 | res_cc.remove("no-store"); 234 | res_cc.remove("must-revalidate"); 235 | res.insert( 236 | "cache-control", 237 | HeaderValue::from_str(&format_cache_control(&res_cc)).unwrap(), 238 | ); 239 | res.remove("expires"); 240 | res.remove("pragma"); 241 | } 242 | 243 | // When the Cache-Control header field is not present in a request, caches MUST consider the no-cache request pragma-directive 244 | // as having the same effect as if "Cache-Control: no-cache" were present (see Section 5.2.1). 245 | if !res.contains_key("cache-control") 246 | && res 247 | .get_str("pragma") 248 | .map_or(false, |p| p.contains("no-cache")) 249 | { 250 | res_cc.insert("no-cache".into(), None); 251 | } 252 | 253 | Self { req, res, uri, status, method, opts, res_cc, req_cc, response_time } 254 | } 255 | 256 | /// Returns `true` if the response can be stored in a cache. If it's 257 | /// `false` then you MUST NOT store either the request or the response. 258 | pub fn is_storable(&self) -> bool { 259 | // The "no-store" request directive indicates that a cache MUST NOT store any part of either this request or any response to it. 260 | !self.req_cc.contains_key("no-store") && 261 | // A cache MUST NOT store a response to any request, unless: 262 | // The request method is understood by the cache and defined as being cacheable, and 263 | (Method::GET == self.method || 264 | Method::HEAD == self.method || 265 | (Method::POST == self.method && self.has_explicit_expiration())) && 266 | // the response status code is understood by the cache, and 267 | UNDERSTOOD_STATUSES.contains(&self.status.as_u16()) && 268 | // the "no-store" cache directive does not appear in request or response header fields, and 269 | !self.res_cc.contains_key("no-store") && 270 | // the "private" response directive does not appear in the response, if the cache is shared, and 271 | (!self.opts.shared || !self.res_cc.contains_key("private")) && 272 | // the Authorization header field does not appear in the request, if the cache is shared, 273 | (!self.opts.shared || 274 | !self.req.contains_key("authorization") || 275 | self.allows_storing_authenticated()) && 276 | // the response either: 277 | // contains an Expires header field, or 278 | (self.res.contains_key("expires") || 279 | // contains a max-age response directive, or 280 | // contains a s-maxage response directive and the cache is shared, or 281 | // contains a public response directive. 282 | self.res_cc.contains_key("max-age") || 283 | (self.opts.shared && self.res_cc.contains_key("s-maxage")) || 284 | self.res_cc.contains_key("public") || 285 | // has a status code that is defined as cacheable by default 286 | STATUS_CODE_CACHEABLE_BY_DEFAULT.contains(&self.status.as_u16())) 287 | } 288 | 289 | fn has_explicit_expiration(&self) -> bool { 290 | // 4.2.1 Calculating Freshness Lifetime 291 | (self.opts.shared && self.res_cc.contains_key("s-maxage")) 292 | || self.res_cc.contains_key("max-age") 293 | || self.res.contains_key("expires") 294 | } 295 | 296 | /// Returns whether the cached response is still fresh in the context of 297 | /// the new request. 298 | /// 299 | /// If it returns `Fresh`, then the given request matches the original 300 | /// response this cache policy has been created with, and the response can 301 | /// be reused without contacting the server. 302 | /// 303 | /// If it returns `Stale`, then the response may not be matching at all 304 | /// (e.g. it's for a different URL or method), or may require to be 305 | /// refreshed first. Either way, the new request's headers will have been 306 | /// updated for sending it to the origin server. 307 | pub fn before_request(&self, req: &Req, now: SystemTime) -> BeforeRequest { 308 | let req_headers = req.headers(); 309 | 310 | // revalidation allowed via HEAD 311 | let (matches, may_revalidate) = self.request_matches(req); 312 | 313 | if matches && self.satisfies_without_revalidation(req_headers, now) { 314 | BeforeRequest::Fresh(self.cached_response(now)) 315 | } else if may_revalidate { 316 | BeforeRequest::Stale { 317 | request: self.revalidation_request(req), 318 | matches, 319 | } 320 | } else { 321 | BeforeRequest::Stale { 322 | request: self.request_from_headers(req_headers.clone()), 323 | matches, 324 | } 325 | } 326 | } 327 | 328 | fn satisfies_without_revalidation(&self, req_headers: &HeaderMap, now: SystemTime) -> bool { 329 | // When presented with a request, a cache MUST NOT reuse a stored response, unless: 330 | // the presented request does not contain the no-cache pragma (Section 5.4), nor the no-cache cache directive, 331 | // unless the stored response is successfully validated (Section 4.3), and 332 | let req_cc = parse_cache_control(req_headers.get_all("cache-control")); 333 | if req_cc.contains_key("no-cache") 334 | || req_headers 335 | .get_str("pragma") 336 | .map_or(false, |v| v.contains("no-cache")) 337 | { 338 | return false; 339 | } 340 | 341 | if let Some(max_age) = req_cc 342 | .get("max-age") 343 | .and_then(|v| v.as_ref()) 344 | .and_then(|p| p.parse().ok()) 345 | { 346 | if self.age(now) > Duration::from_secs(max_age) { 347 | return false; 348 | } 349 | } 350 | 351 | if let Some(min_fresh) = req_cc 352 | .get("min-fresh") 353 | .and_then(|v| v.as_ref()) 354 | .and_then(|p| p.parse().ok()) 355 | { 356 | if self.time_to_live(now) < Duration::from_secs(min_fresh) { 357 | return false; 358 | } 359 | } 360 | 361 | // the stored response is either: 362 | // fresh, or allowed to be served stale 363 | if self.is_stale(now) { 364 | // If no value is assigned to max-stale, then the client is willing to accept a stale response of any age. 365 | let max_stale = req_cc.get("max-stale"); 366 | let has_max_stale = max_stale.is_some(); 367 | let max_stale = max_stale 368 | .and_then(|m| m.as_ref()) 369 | .and_then(|s| s.parse().ok()); 370 | let allows_stale = !self.res_cc.contains_key("must-revalidate") 371 | && has_max_stale 372 | && max_stale.map_or(true, |val| { 373 | Duration::from_secs(val) > self.age(now) - self.max_age() 374 | }); 375 | if !allows_stale { 376 | return false; 377 | } 378 | } 379 | 380 | true 381 | } 382 | 383 | /// returns: matches including method, matches allowing head 384 | fn request_matches(&self, req: &Req) -> (bool, bool) { 385 | // The presented effective request URI and that of the stored response match, and 386 | let matches = req.is_same_uri(&self.uri) && 387 | (self.req.get("host") == req.headers().get("host")) && 388 | // selecting header fields nominated by the stored response (if any) match those presented, and 389 | self.vary_matches(req); 390 | let exact_match = matches && self.method == req.method(); 391 | 392 | // the request method associated with the stored response allows it to be used for the presented request, and 393 | (exact_match, exact_match || Method::HEAD == req.method()) 394 | } 395 | 396 | fn allows_storing_authenticated(&self) -> bool { 397 | // following Cache-Control response directives (Section 5.2.2) have such an effect: must-revalidate, public, and s-maxage. 398 | self.res_cc.contains_key("must-revalidate") 399 | || self.res_cc.contains_key("public") 400 | || self.res_cc.contains_key("s-maxage") 401 | } 402 | 403 | fn vary_matches(&self, req: &Req) -> bool { 404 | for name in get_all_comma(self.res.get_all("vary")) { 405 | // A Vary header field-value of "*" always fails to match 406 | if name == "*" { 407 | return false; 408 | } 409 | let name = name.trim().to_ascii_lowercase(); 410 | if req.headers().get(&name) != self.req.get(&name) { 411 | return false; 412 | } 413 | } 414 | true 415 | } 416 | 417 | fn copy_without_hop_by_hop_headers(in_headers: &HeaderMap) -> HeaderMap { 418 | let mut headers = HeaderMap::with_capacity(in_headers.len()); 419 | 420 | for (h, v) in in_headers 421 | .iter() 422 | .filter(|(h, _)| !HOP_BY_HOP_HEADERS.contains(&h.as_str())) 423 | { 424 | headers.insert(h.clone(), v.clone()); 425 | } 426 | 427 | // 9.1. Connection 428 | for name in get_all_comma(in_headers.get_all("connection")) { 429 | headers.remove(name); 430 | } 431 | 432 | let new_warnings = join( 433 | get_all_comma(in_headers.get_all("warning")).filter(|warning| { 434 | !warning.trim_start().starts_with('1') // FIXME: match 100-199, not 1 or 1000 435 | }), 436 | ); 437 | if new_warnings.is_empty() { 438 | headers.remove("warning"); 439 | } else { 440 | headers.insert("warning", HeaderValue::from_str(&new_warnings).unwrap()); 441 | } 442 | headers 443 | } 444 | 445 | /// Updates and filters the response headers for a cached response before 446 | /// returning it to a client. This function is necessary, because proxies 447 | /// MUST always remove hop-by-hop headers (such as TE and Connection) and 448 | /// update response's Age to avoid doubling cache time. 449 | /// 450 | /// It returns response "parts" without a body. You can upgrade it to a full 451 | /// response with `Response::from_parts(parts, BYOB)` 452 | fn cached_response(&self, now: SystemTime) -> http::response::Parts { 453 | let mut headers = Self::copy_without_hop_by_hop_headers(&self.res); 454 | let age = self.age(now); 455 | let day = Duration::from_secs(3600 * 24); 456 | 457 | // A cache SHOULD generate 113 warning if it heuristically chose a freshness 458 | // lifetime greater than 24 hours and the response's age is greater than 24 hours. 459 | if age > day && !self.has_explicit_expiration() && self.max_age() > day { 460 | headers.append( 461 | "warning", 462 | HeaderValue::from_static(r#"113 - "rfc7234 5.5.4""#), 463 | ); 464 | } 465 | let date = OffsetDateTime::from(now); 466 | headers.insert( 467 | "age", 468 | HeaderValue::from_str(&age.as_secs().to_string()).unwrap(), 469 | ); 470 | headers.insert( 471 | "date", 472 | HeaderValue::from_str(&date.format(&Rfc2822).unwrap()).unwrap(), 473 | ); 474 | 475 | let mut parts = Response::builder() 476 | .status(self.status) 477 | .body(()) 478 | .unwrap() 479 | .into_parts().0; 480 | parts.headers = headers; 481 | parts 482 | } 483 | 484 | fn raw_server_date(&self) -> SystemTime { 485 | let date = self 486 | .res 487 | .get_str("date") 488 | .and_then(|d| OffsetDateTime::parse(d, &Rfc2822).ok()) 489 | .and_then(|d| { 490 | SystemTime::UNIX_EPOCH.checked_add(Duration::from_secs(d.unix_timestamp() as u64)) 491 | }); 492 | date.unwrap_or(self.response_time) 493 | } 494 | 495 | /// Tells how long the response has been sitting in cache(s). 496 | /// 497 | /// Value of the `Age` header, updated for the current time. 498 | pub fn age(&self, now: SystemTime) -> Duration { 499 | let mut age = self.age_header_value(); 500 | 501 | if let Ok(resident_time) = now.duration_since(self.response_time) { 502 | age += resident_time; 503 | } 504 | age 505 | } 506 | 507 | fn age_header_value(&self) -> Duration { 508 | Duration::from_secs( 509 | self.res 510 | .get_str("age") 511 | .and_then(|v| v.parse().ok()) 512 | .unwrap_or(0), 513 | ) 514 | } 515 | 516 | /// Value of applicable max-age (or heuristic equivalent) in seconds. 517 | /// 518 | /// This counts since response's `Date` - `Age`. 519 | /// 520 | /// For an up-to-date value, see `time_to_live()`. 521 | fn max_age(&self) -> Duration { 522 | if !self.is_storable() || self.res_cc.contains_key("no-cache") { 523 | return Duration::from_secs(0); 524 | } 525 | 526 | // Shared responses with cookies are cacheable according to the RFC, but IMHO it'd be unwise to do so by default 527 | // so this implementation requires explicit opt-in via public header 528 | if self.opts.shared 529 | && (self.res.contains_key("set-cookie") 530 | && !self.res_cc.contains_key("public") 531 | && !self.res_cc.contains_key("immutable")) 532 | { 533 | return Duration::from_secs(0); 534 | } 535 | 536 | if self.res.get_str("vary").map(str::trim) == Some("*") { 537 | return Duration::from_secs(0); 538 | } 539 | 540 | if self.opts.shared { 541 | if self.res_cc.contains_key("proxy-revalidate") { 542 | return Duration::from_secs(0); 543 | } 544 | // if a response includes the s-maxage directive, a shared cache recipient MUST ignore the Expires field. 545 | if let Some(s_max) = self.res_cc.get("s-maxage").and_then(|v| v.as_ref()) { 546 | return Duration::from_secs(s_max.parse().unwrap_or(0)); 547 | } 548 | } 549 | 550 | // If a response includes a Cache-Control field with the max-age directive, a recipient MUST ignore the Expires field. 551 | if let Some(max_age) = self.res_cc.get("max-age").and_then(|v| v.as_ref()) { 552 | return Duration::from_secs(max_age.parse().unwrap_or(0)); 553 | } 554 | 555 | let default_min_ttl = if self.res_cc.contains_key("immutable") { 556 | self.opts.immutable_min_time_to_live 557 | } else { 558 | Duration::from_secs(0) 559 | }; 560 | 561 | let server_date = self.raw_server_date(); 562 | if let Some(expires) = self.res.get_str("expires") { 563 | return match OffsetDateTime::parse(expires, &Rfc2822) { 564 | // A cache recipient MUST interpret invalid date formats, especially the value "0", as representing a time in the past (i.e., "already expired"). 565 | Err(_) => Duration::from_secs(0), 566 | Ok(expires) => { 567 | let expires = SystemTime::UNIX_EPOCH 568 | + Duration::from_secs(expires.unix_timestamp().max(0) as _); 569 | return default_min_ttl 570 | .max(expires.duration_since(server_date).unwrap_or_default()); 571 | } 572 | }; 573 | } 574 | 575 | if let Some(last_modified) = self.res.get_str("last-modified") { 576 | if let Ok(last_modified) = OffsetDateTime::parse(last_modified, &Rfc2822) { 577 | let last_modified = SystemTime::UNIX_EPOCH 578 | + Duration::from_secs(last_modified.unix_timestamp().max(0) as _); 579 | if let Ok(diff) = server_date.duration_since(last_modified) { 580 | let secs_left = diff.as_secs() as f64 * f64::from(self.opts.cache_heuristic); 581 | return default_min_ttl.max(Duration::from_secs(secs_left as _)); 582 | } 583 | } 584 | } 585 | 586 | default_min_ttl 587 | } 588 | 589 | /// Returns approximate time until the response becomes 590 | /// stale (i.e. not fresh). This is the correct way of getting the current `max-age` value. 591 | /// 592 | /// After that time (when `time_to_live() == Duration::ZERO`) the response might not be 593 | /// usable without revalidation. However, there are exceptions, e.g. a 594 | /// client can explicitly allow stale responses, so always check with 595 | /// `before_request()`. 596 | /// 597 | /// If you're storing responses in a cache/database, keep them approximately for 598 | /// the `time_to_live` duration plus some extra time to allow for revalidation 599 | /// (an expired response is still useful). 600 | pub fn time_to_live(&self, now: SystemTime) -> Duration { 601 | self.max_age() 602 | .checked_sub(self.age(now)) 603 | .unwrap_or_default() 604 | } 605 | 606 | /// Stale responses shouldn't be used without contacting the server (revalidation) 607 | pub fn is_stale(&self, now: SystemTime) -> bool { 608 | self.max_age() <= self.age(now) 609 | } 610 | 611 | /// Headers for sending to the origin server to revalidate stale response. 612 | /// Allows server to return 304 to allow reuse of the previous response. 613 | /// 614 | /// Hop by hop headers are always stripped. 615 | /// Revalidation headers may be added or removed, depending on request. 616 | /// 617 | /// It returns request "parts" without a body. You can upgrade it to a full 618 | /// response with `Request::from_parts(parts, BYOB)` (the body is usually `()`). 619 | /// 620 | /// You don't need this if you use [`before_request()`] 621 | fn revalidation_request(&self, incoming_req: &Req) -> http::request::Parts { 622 | let mut headers = Self::copy_without_hop_by_hop_headers(incoming_req.headers()); 623 | 624 | // This implementation does not understand range requests 625 | headers.remove("if-range"); 626 | 627 | if !self.is_storable() { 628 | // not for the same resource, or wasn't allowed to be cached anyway 629 | headers.remove("if-none-match"); 630 | headers.remove("if-modified-since"); 631 | return self.request_from_headers(headers); 632 | } 633 | 634 | /* MUST send that entity-tag in any cache validation request (using If-Match or If-None-Match) if an entity-tag has been provided by the origin server. */ 635 | if let Some(etag) = self.res.get_str("etag") { 636 | let if_none = join(get_all_comma(headers.get_all("if-none-match")).chain(Some(etag))); 637 | headers.insert("if-none-match", HeaderValue::from_str(&if_none).unwrap()); 638 | } 639 | 640 | // Clients MAY issue simple (non-subrange) GET requests with either weak validators or strong validators. Clients MUST NOT use weak validators in other forms of request. 641 | let forbids_weak_validators = self.method != Method::GET 642 | || headers.contains_key("accept-ranges") 643 | || headers.contains_key("if-match") 644 | || headers.contains_key("if-unmodified-since"); 645 | 646 | /* SHOULD send the Last-Modified value in non-subrange cache validation requests (using If-Modified-Since) if only a Last-Modified value has been provided by the origin server. 647 | Note: This implementation does not understand partial responses (206) */ 648 | if forbids_weak_validators { 649 | headers.remove("if-modified-since"); 650 | 651 | let etags = join( 652 | get_all_comma(headers.get_all("if-none-match")) 653 | .filter(|etag| !etag.trim_start().starts_with("W/")), 654 | ); 655 | if etags.is_empty() { 656 | headers.remove("if-none-match"); 657 | } else { 658 | headers.insert("if-none-match", HeaderValue::from_str(&etags).unwrap()); 659 | } 660 | } else if !headers.contains_key("if-modified-since") { 661 | if let Some(last_modified) = self.res.get_str("last-modified") { 662 | headers.insert( 663 | "if-modified-since", 664 | HeaderValue::from_str(last_modified).unwrap(), 665 | ); 666 | } 667 | } 668 | self.request_from_headers(headers) 669 | } 670 | 671 | fn request_from_headers(&self, headers: HeaderMap) -> http::request::Parts { 672 | let mut parts = Request::builder() 673 | .method(self.method.clone()) 674 | .uri(self.uri.clone()) 675 | .body(()) 676 | .unwrap() 677 | .into_parts().0; 678 | parts.headers = headers; 679 | parts 680 | } 681 | 682 | /// Creates `CachePolicy` with information combined from the previews response, 683 | /// and the new revalidation response. 684 | /// 685 | /// Returns `{policy, modified}` where modified is a boolean indicating 686 | /// whether the response body has been modified, and old cached body can't be used. 687 | pub fn after_response( 688 | &self, 689 | request: &Req, 690 | response: &Res, 691 | response_time: SystemTime, 692 | ) -> AfterResponse { 693 | let response_headers = response.headers(); 694 | let mut response_status = response.status(); 695 | 696 | let old_etag = &self.res.get_str("etag").map(str::trim); 697 | let old_last_modified = response_headers.get_str("last-modified").map(str::trim); 698 | let new_etag = response_headers.get_str("etag").map(str::trim); 699 | let new_last_modified = response_headers.get_str("last-modified").map(str::trim); 700 | 701 | // These aren't going to be supported exactly, since one CachePolicy object 702 | // doesn't know about all the other cached objects. 703 | let mut matches = false; 704 | if response.status() != StatusCode::NOT_MODIFIED { 705 | matches = false; 706 | } else if new_etag.map_or(false, |etag| !etag.starts_with("W/")) { 707 | // "All of the stored responses with the same strong validator are selected. 708 | // If none of the stored responses contain the same strong validator, 709 | // then the cache MUST NOT use the new response to update any stored responses." 710 | matches = old_etag.map(|e| e.trim_start_matches("W/")) == new_etag; 711 | } else if let (Some(old), Some(new)) = (old_etag, new_etag) { 712 | // "If the new response contains a weak validator and that validator corresponds 713 | // to one of the cache's stored responses, 714 | // then the most recent of those matching stored responses is selected for update." 715 | matches = old.trim_start_matches("W/") == new.trim_start_matches("W/"); 716 | } else if old_last_modified.is_some() { 717 | matches = old_last_modified == new_last_modified; 718 | } else { 719 | // If the new response does not include any form of validator (such as in the case where 720 | // a client generates an If-Modified-Since request from a source other than the Last-Modified 721 | // response header field), and there is only one stored response, and that stored response also 722 | // lacks a validator, then that stored response is selected for update. 723 | if old_etag.is_none() 724 | && new_etag.is_none() 725 | && old_last_modified.is_none() 726 | && new_last_modified.is_none() 727 | { 728 | matches = true; 729 | } 730 | } 731 | 732 | let new_response_headers = if matches { 733 | let mut new_response_headers = HeaderMap::with_capacity(self.res.keys_len()); 734 | // use other header fields provided in the 304 (Not Modified) response to replace all instances 735 | // of the corresponding header fields in the stored response. 736 | for (header, old_value) in &self.res { 737 | let header = header.clone(); 738 | if let Some(new_value) = response_headers.get(&header) { 739 | if !EXCLUDED_FROM_REVALIDATION_UPDATE.contains(&header.as_str()) { 740 | new_response_headers.insert(header, new_value.clone()); 741 | continue; 742 | } 743 | } 744 | new_response_headers.insert(header, old_value.clone()); 745 | } 746 | response_status = self.status; 747 | new_response_headers 748 | } else { 749 | response_headers.clone() 750 | }; 751 | 752 | let new_policy = CachePolicy::from_details( 753 | request.uri(), 754 | request.method().clone(), 755 | response_status, 756 | request.headers().clone(), 757 | new_response_headers, 758 | response_time, 759 | self.opts, 760 | ); 761 | let new_response = new_policy.cached_response(response_time); 762 | 763 | if matches && response.status() == StatusCode::NOT_MODIFIED { 764 | AfterResponse::NotModified(new_policy, new_response) 765 | } else { 766 | AfterResponse::Modified(new_policy, new_response) 767 | } 768 | } 769 | } 770 | 771 | /// New policy and flags to act on `after_response()` 772 | pub enum AfterResponse { 773 | /// You can use the cached body! Make sure to use these updated headers 774 | NotModified(CachePolicy, http::response::Parts), 775 | /// You need to update the body in the cache 776 | Modified(CachePolicy, http::response::Parts), 777 | } 778 | 779 | fn get_all_comma<'a>( 780 | all: impl IntoIterator, 781 | ) -> impl Iterator { 782 | all.into_iter() 783 | .filter_map(|v| v.to_str().ok()) 784 | .flat_map(|s| s.split(',').map(str::trim)) 785 | } 786 | 787 | trait GetHeaderStr { 788 | fn get_str(&self, k: &str) -> Option<&str>; 789 | } 790 | 791 | impl GetHeaderStr for HeaderMap { 792 | #[inline] 793 | fn get_str(&self, k: &str) -> Option<&str> { 794 | self.get(k).and_then(|v| v.to_str().ok()) 795 | } 796 | } 797 | 798 | fn join<'a>(parts: impl Iterator) -> String { 799 | let mut out = String::new(); 800 | for part in parts { 801 | out.reserve(2 + part.len()); 802 | if !out.is_empty() { 803 | out.push_str(", "); 804 | } 805 | out.push_str(part); 806 | } 807 | out 808 | } 809 | 810 | /// Next action suggested after `before_request()` 811 | pub enum BeforeRequest { 812 | /// Good news! You can use it with body from the cache. No need to contact the server. 813 | Fresh(http::response::Parts), 814 | /// You must send the request to the server first. 815 | Stale { 816 | /// Send this request to the server (it has added revalidation headers when appropriate) 817 | request: http::request::Parts, 818 | /// If `false`, request was for some other resource that isn't 819 | /// semantically the same as previously cached request+response 820 | matches: bool, 821 | }, 822 | } 823 | 824 | impl BeforeRequest { 825 | /// For backwards compatibility only. 826 | /// Don't forget to use request headers from `BeforeRequest::Fresh` 827 | pub fn satisfies_without_revalidation(&self) -> bool { 828 | matches!(self, Self::Fresh(_)) 829 | } 830 | } 831 | 832 | /// Allows using either `Request` or `request::Parts`, or your own newtype. 833 | pub trait RequestLike { 834 | /// Same as `req.uri().clone()` 835 | fn uri(&self) -> Uri; 836 | /// Whether the effective request URI matches the other URI 837 | /// 838 | /// It can be naive string comparison, nothing fancy 839 | fn is_same_uri(&self, other: &Uri) -> bool; 840 | /// Same as `req.method()` 841 | fn method(&self) -> &Method; 842 | /// Same as `req.headers()` 843 | fn headers(&self) -> &HeaderMap; 844 | } 845 | 846 | /// Allows using either `Response` or `response::Parts`, or your own newtype. 847 | pub trait ResponseLike { 848 | /// Same as `res.status()` 849 | fn status(&self) -> StatusCode; 850 | /// Same as `res.headers()` 851 | fn headers(&self) -> &HeaderMap; 852 | } 853 | 854 | impl RequestLike for Request { 855 | fn uri(&self) -> Uri { 856 | self.uri().clone() 857 | } 858 | fn is_same_uri(&self, other: &Uri) -> bool { 859 | self.uri() == other 860 | } 861 | fn method(&self) -> &Method { 862 | self.method() 863 | } 864 | fn headers(&self) -> &HeaderMap { 865 | self.headers() 866 | } 867 | } 868 | 869 | impl RequestLike for http::request::Parts { 870 | fn uri(&self) -> Uri { 871 | self.uri.clone() 872 | } 873 | fn is_same_uri(&self, other: &Uri) -> bool { 874 | &self.uri == other 875 | } 876 | fn method(&self) -> &Method { 877 | &self.method 878 | } 879 | fn headers(&self) -> &HeaderMap { 880 | &self.headers 881 | } 882 | } 883 | 884 | impl ResponseLike for Response { 885 | fn status(&self) -> StatusCode { 886 | self.status() 887 | } 888 | fn headers(&self) -> &HeaderMap { 889 | self.headers() 890 | } 891 | } 892 | 893 | impl ResponseLike for http::response::Parts { 894 | fn status(&self) -> StatusCode { 895 | self.status 896 | } 897 | fn headers(&self) -> &HeaderMap { 898 | &self.headers 899 | } 900 | } 901 | 902 | #[cfg(feature = "reqwest")] 903 | impl RequestLike for reqwest::Request { 904 | fn uri(&self) -> Uri { 905 | self.url().as_str().parse().expect("Uri and Url are incompatible!?") 906 | } 907 | fn is_same_uri(&self, other: &Uri) -> bool { 908 | self.url().as_str() == other 909 | } 910 | fn method(&self) -> &Method { 911 | self.method() 912 | } 913 | fn headers(&self) -> &HeaderMap { 914 | self.headers() 915 | } 916 | } 917 | 918 | #[cfg(feature = "reqwest")] 919 | impl ResponseLike for reqwest::Response { 920 | fn status(&self) -> StatusCode { 921 | self.status() 922 | } 923 | fn headers(&self) -> &HeaderMap { 924 | self.headers() 925 | } 926 | } 927 | -------------------------------------------------------------------------------- /tests/tests.rs: -------------------------------------------------------------------------------- 1 | //! Determines whether a given HTTP response can be cached and whether a 2 | //! cached response can be reused, following the rules specified in [RFC 3 | //! 7234](https://httpwg.org/specs/rfc7234.html). 4 | 5 | use http::header::HeaderName; 6 | use http::header::HeaderValue; 7 | use http::Request; 8 | use http::Response; 9 | use http_cache_semantics::*; 10 | use serde_json::json; 11 | use serde_json::Value; 12 | use std::time::SystemTime; 13 | use time::format_description::well_known::Rfc2822; 14 | use time::OffsetDateTime; 15 | 16 | fn res(json: Value) -> Response<()> { 17 | let mut res = Response::builder() 18 | .status(json.get("status").and_then(|s| s.as_i64()).unwrap_or(200) as u16); 19 | if let Some(map) = json.get("headers").and_then(|s| s.as_object()) { 20 | for (k, v) in map { 21 | let v = v.as_str().unwrap(); 22 | res = res.header( 23 | HeaderName::from_bytes(k.as_bytes()).unwrap(), 24 | HeaderValue::from_str(v).unwrap(), 25 | ); 26 | } 27 | } 28 | res.body(()).unwrap() 29 | } 30 | 31 | fn req(json: Value) -> Request<()> { 32 | let mut req = Request::builder() 33 | .method(json.get("method").and_then(|s| s.as_str()).unwrap_or("GET")) 34 | .uri( 35 | json.get("uri") 36 | .and_then(|s| s.as_str()) 37 | .unwrap_or("http://example.com"), 38 | ); 39 | if let Some(map) = json.get("headers").and_then(|s| s.as_object()) { 40 | for (k, v) in map { 41 | let v = v.as_str().unwrap(); 42 | req = req.header( 43 | HeaderName::from_bytes(k.as_bytes()).unwrap(), 44 | HeaderValue::from_str(v).unwrap(), 45 | ); 46 | } 47 | } 48 | req.body(()).unwrap() 49 | } 50 | 51 | fn assert_cached(should_put: bool, response_code: i32) { 52 | let now = SystemTime::now(); 53 | let mut response = json!({ 54 | "headers": { 55 | "last-modified": format_date(-105, 1), 56 | "expires": format_date(1, 3600), 57 | "www-authenticate": "challenge" 58 | }, 59 | "status": response_code, 60 | }); 61 | 62 | if 407 == response_code { 63 | response["headers"]["proxy-authenticate"] = json!("Basic realm=\"protected area\""); 64 | } else if 401 == response_code { 65 | response["headers"]["www-authenticate"] = json!("Basic realm=\"protected area\""); 66 | } 67 | 68 | let policy = CachePolicy::new_options( 69 | &Request::get("http://example.com").body(()).unwrap(), 70 | &res(response), 71 | now, 72 | CacheOptions { 73 | shared: false, 74 | ..Default::default() 75 | }, 76 | ); 77 | 78 | assert_eq!( 79 | should_put, 80 | policy.is_storable(), 81 | "{should_put}; {response_code}; {policy:#?}" 82 | ); 83 | } 84 | 85 | #[test] 86 | fn test_ok_http_response_caching_by_response_code() { 87 | assert_cached(false, 100); 88 | assert_cached(false, 101); 89 | assert_cached(false, 102); 90 | assert_cached(true, 200); 91 | assert_cached(false, 201); 92 | assert_cached(false, 202); 93 | assert_cached(true, 203); 94 | assert_cached(true, 204); 95 | assert_cached(false, 205); 96 | // 206: electing to not cache partial responses 97 | assert_cached(false, 206); 98 | assert_cached(false, 207); 99 | assert_cached(true, 300); 100 | assert_cached(true, 301); 101 | assert_cached(true, 302); 102 | assert_cached(false, 304); 103 | assert_cached(false, 305); 104 | assert_cached(false, 306); 105 | assert_cached(true, 307); 106 | assert_cached(true, 308); 107 | assert_cached(false, 400); 108 | assert_cached(false, 401); 109 | assert_cached(false, 402); 110 | assert_cached(false, 403); 111 | assert_cached(true, 404); 112 | assert_cached(true, 405); 113 | assert_cached(false, 406); 114 | assert_cached(false, 408); 115 | assert_cached(false, 409); 116 | // 410: the HTTP spec permits caching 410s, but the RI doesn't 117 | assert_cached(true, 410); 118 | assert_cached(false, 411); 119 | assert_cached(false, 412); 120 | assert_cached(false, 413); 121 | assert_cached(true, 414); 122 | assert_cached(false, 415); 123 | assert_cached(false, 416); 124 | assert_cached(false, 417); 125 | assert_cached(false, 418); 126 | assert_cached(false, 429); 127 | assert_cached(false, 500); 128 | assert_cached(true, 501); 129 | assert_cached(false, 502); 130 | assert_cached(false, 503); 131 | assert_cached(false, 504); 132 | assert_cached(false, 505); 133 | assert_cached(false, 506); 134 | } 135 | 136 | #[test] 137 | fn test_default_expiration_date_fully_cached_for_less_than_24_hours() { 138 | let now = SystemTime::now(); 139 | let policy = CachePolicy::new_options( 140 | &Request::get("http://example.com").body(()).unwrap(), 141 | &res(json!({ 142 | "headers": { 143 | "last-modified": format_date(-105, 1), 144 | "date": format_date(-5, 1), 145 | }, 146 | "body": "A" 147 | })), 148 | now, 149 | CacheOptions { 150 | shared: false, 151 | ..Default::default() 152 | }, 153 | ); 154 | assert!(policy.time_to_live(now).as_secs() >= 4); 155 | } 156 | 157 | #[test] 158 | fn test_default_expiration_date_fully_cached_for_more_than_24_hours() { 159 | let now = SystemTime::now(); 160 | let policy = CachePolicy::new_options( 161 | &Request::get("http://example.com").body(()).unwrap(), 162 | &res(json!({ 163 | "headers": { 164 | "last-modified": format_date(-105, 3600 * 24), 165 | "date": format_date(-5, 3600 * 24), 166 | }, 167 | "body": "A" 168 | })), 169 | now, 170 | CacheOptions { 171 | shared: false, 172 | ..Default::default() 173 | }, 174 | ); 175 | assert!((policy.time_to_live(now) + policy.age(now)).as_secs() >= 10 * 3600 * 24); 176 | assert!(policy.time_to_live(now).as_secs() >= 5 * 3600 * 24 - 1); 177 | } 178 | 179 | #[test] 180 | fn test_max_age_preferred_over_lower_shared_max_age() { 181 | let now = SystemTime::now(); 182 | let policy = CachePolicy::new_options( 183 | &Request::get("http://example.com").body(()).unwrap(), 184 | &res(json!({ 185 | "headers": { 186 | "date": format_date(-2, 60), 187 | "cache-control": "s-maxage=60, max-age=180", 188 | } 189 | })), 190 | now, 191 | CacheOptions { 192 | shared: false, 193 | ..Default::default() 194 | }, 195 | ); 196 | assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 180); 197 | } 198 | 199 | fn request_method_not_cached(method: String) { 200 | // 1. seed the cache (potentially) 201 | // 2. expect a cache hit or miss 202 | let now = SystemTime::now(); 203 | let policy = CachePolicy::new_options( 204 | &req(json!({ 205 | "method": method, 206 | "headers": {} 207 | })), 208 | &res(json!({ 209 | "headers": { 210 | "expires": format_date(1, 3600), 211 | } 212 | })), 213 | now, 214 | CacheOptions { 215 | shared: false, 216 | ..Default::default() 217 | }, 218 | ); 219 | assert!(policy.is_stale(now)); 220 | } 221 | 222 | #[test] 223 | fn test_request_method_options_is_not_cached() { 224 | request_method_not_cached("OPTIONS".to_string()); 225 | } 226 | 227 | #[test] 228 | fn test_request_method_put_is_not_cached() { 229 | request_method_not_cached("PUT".to_string()); 230 | } 231 | 232 | #[test] 233 | fn test_request_method_delete_is_not_cached() { 234 | request_method_not_cached("DELETE".to_string()); 235 | } 236 | 237 | #[test] 238 | fn test_request_method_trace_is_not_cached() { 239 | request_method_not_cached("TRACE".to_string()); 240 | } 241 | 242 | #[test] 243 | fn test_etag_and_expiration_date_in_the_future() { 244 | let now = SystemTime::now(); 245 | let policy = CachePolicy::new_options( 246 | &Request::get("http://example.com").body(()).unwrap(), 247 | &res(json!({ 248 | "headers": { 249 | "etag": "v1", 250 | "last-modified": format_date(-2, 3600), 251 | "expires": format_date(1, 3600), 252 | } 253 | })), 254 | now, 255 | CacheOptions { 256 | shared: false, 257 | ..Default::default() 258 | }, 259 | ); 260 | assert!(policy.time_to_live(now).as_secs() > 0); 261 | } 262 | 263 | #[test] 264 | fn test_client_side_no_store() { 265 | let now = SystemTime::now(); 266 | let policy = CachePolicy::new_options( 267 | &req(json!({ 268 | "headers": { 269 | "cache-control": "no-store", 270 | } 271 | })), 272 | &res(json!({ 273 | "headers": { 274 | "cache-control": "max-age=60", 275 | } 276 | })), 277 | now, 278 | CacheOptions { 279 | shared: false, 280 | ..Default::default() 281 | }, 282 | ); 283 | assert_eq!(policy.is_storable(), false); 284 | } 285 | 286 | #[test] 287 | fn test_request_min_fresh() { 288 | let now = SystemTime::now(); 289 | let policy = CachePolicy::new_options( 290 | &Request::get("http://example.com").body(()).unwrap(), 291 | &res(json!({ 292 | "headers": { 293 | "cache-control": "max-age=60", 294 | } 295 | })), 296 | now, 297 | CacheOptions { 298 | shared: false, 299 | ..Default::default() 300 | }, 301 | ); 302 | assert_eq!(policy.is_stale(now), false); 303 | 304 | assert!(policy 305 | .before_request( 306 | &req(json!({ 307 | "headers": { 308 | "cache-control": "min-fresh=10", 309 | }, 310 | })), 311 | now 312 | ) 313 | .satisfies_without_revalidation()); 314 | 315 | assert_eq!( 316 | policy 317 | .before_request( 318 | &req(json!({ 319 | "headers": { 320 | "cache-control": "min-fresh=120", 321 | }, 322 | })), 323 | now 324 | ) 325 | .satisfies_without_revalidation(), 326 | false 327 | ); 328 | } 329 | 330 | #[test] 331 | fn test_do_not_cache_partial_response() { 332 | let policy = CachePolicy::new( 333 | &Request::get("http://example.com").body(()).unwrap(), 334 | &res(json!({ 335 | "status": 206, 336 | "headers": { 337 | "content-range": "bytes 100-100/200", 338 | "cache-control": "max-age=60", 339 | } 340 | })), 341 | ); 342 | 343 | assert_eq!(policy.is_storable(), false); 344 | } 345 | 346 | fn format_date(delta: i64, unit: i64) -> String { 347 | let now = OffsetDateTime::now_utc(); 348 | let timestamp = now.unix_timestamp() + delta * unit; 349 | 350 | let date = OffsetDateTime::from_unix_timestamp(timestamp).unwrap(); 351 | date.format(&Rfc2822).unwrap() 352 | } 353 | 354 | #[test] 355 | fn test_no_store_kills_cache() { 356 | let now = SystemTime::now(); 357 | let policy = CachePolicy::new( 358 | &req(json!({ 359 | "method": "GET", 360 | "headers": { 361 | "cache-control": "no-store", 362 | } 363 | })), 364 | &res(json!({ 365 | "headers": { 366 | "cache-control": "public, max-age=222", 367 | } 368 | })), 369 | ); 370 | 371 | assert!(policy.is_stale(now)); 372 | assert_eq!(policy.is_storable(), false); 373 | } 374 | 375 | #[test] 376 | fn test_post_not_cacheable_by_default() { 377 | let now = SystemTime::now(); 378 | let policy = CachePolicy::new( 379 | &req(json!({ 380 | "method": "POST", 381 | "headers": {}, 382 | })), 383 | &res(json!({ 384 | "headers": { 385 | "cache-control": "public", 386 | } 387 | })), 388 | ); 389 | 390 | assert!(policy.is_stale(now)); 391 | assert_eq!(policy.is_storable(), false); 392 | } 393 | 394 | #[test] 395 | fn test_post_cacheable_explicitly() { 396 | let now = SystemTime::now(); 397 | let policy = CachePolicy::new( 398 | &req(json!({ 399 | "method": "POST", 400 | "headers": {}, 401 | })), 402 | &res(json!({ 403 | "headers": { 404 | "cache-control": "public, max-age=222", 405 | } 406 | })), 407 | ); 408 | 409 | assert_eq!(policy.is_stale(now), false); 410 | assert!(policy.is_storable()); 411 | } 412 | 413 | #[test] 414 | fn test_public_cacheable_auth_is_ok() { 415 | let now = SystemTime::now(); 416 | let policy = CachePolicy::new( 417 | &req(json!({ 418 | "method": "GET", 419 | "headers": { 420 | "authorization": "test", 421 | } 422 | })), 423 | &res(json!({ 424 | "headers": { 425 | "cache-control": "public, max-age=222", 426 | } 427 | })), 428 | ); 429 | 430 | assert_eq!(policy.is_stale(now), false); 431 | assert!(policy.is_storable()); 432 | } 433 | 434 | #[test] 435 | fn test_proxy_cacheable_auth_is_ok() { 436 | let now = SystemTime::now(); 437 | let policy = CachePolicy::new( 438 | &req(json!({ 439 | "method": "GET", 440 | "headers": { 441 | "authorization": "test", 442 | } 443 | })), 444 | &res(json!({ 445 | "headers": { 446 | "cache-control": "max-age=0,s-maxage=12", 447 | } 448 | })), 449 | ); 450 | 451 | assert_eq!(policy.is_stale(now), false); 452 | assert!(policy.is_storable()); 453 | 454 | #[cfg(feature = "serde")] 455 | { 456 | let json = serde_json::to_string(&policy).unwrap(); 457 | let policy: CachePolicy = serde_json::from_str(&json).unwrap(); 458 | 459 | assert!(!policy.is_stale(now)); 460 | assert!(policy.is_storable()); 461 | } 462 | } 463 | 464 | #[test] 465 | fn test_private_auth_is_ok() { 466 | let now = SystemTime::now(); 467 | let policy = CachePolicy::new_options( 468 | &req(json!({ 469 | "method": "GET", 470 | "headers": { 471 | "authorization": "test", 472 | } 473 | })), 474 | &res(json!({ 475 | "headers": { 476 | "cache-control": "max-age=111", 477 | } 478 | })), 479 | now, 480 | CacheOptions { 481 | shared: false, 482 | ..Default::default() 483 | }, 484 | ); 485 | assert_eq!(policy.is_stale(now), false); 486 | assert!(policy.is_storable()); 487 | } 488 | 489 | #[test] 490 | fn test_revalidate_auth_is_ok() { 491 | let policy = CachePolicy::new( 492 | &req(json!({ 493 | "method": "GET", 494 | "headers": { 495 | "authorization": "test", 496 | } 497 | })), 498 | &res(json!({ 499 | "headers": { 500 | "cache-control": "max-age=88,must-revalidate", 501 | } 502 | })), 503 | ); 504 | 505 | assert!(policy.is_storable()); 506 | } 507 | 508 | #[test] 509 | fn test_auth_prevents_caching_by_default() { 510 | let now = SystemTime::now(); 511 | let policy = CachePolicy::new( 512 | &req(json!({ 513 | "method": "GET", 514 | "headers": { 515 | "authorization": "test", 516 | } 517 | })), 518 | &res(json!({ 519 | "headers": { 520 | "cache-control": "max-age=111", 521 | } 522 | })), 523 | ); 524 | 525 | assert!(policy.is_stale(now)); 526 | assert_eq!(policy.is_storable(), false); 527 | } 528 | 529 | #[test] 530 | fn test_simple_miss() { 531 | let now = SystemTime::now(); 532 | let policy = CachePolicy::new( 533 | &req(json!({ 534 | "method": "GET", 535 | "headers": {}, 536 | })), 537 | &res(json!({})), 538 | ); 539 | 540 | assert!(policy.is_stale(now)); 541 | } 542 | 543 | #[test] 544 | fn test_simple_hit() { 545 | let now = SystemTime::now(); 546 | let policy = CachePolicy::new( 547 | &req(json!({ 548 | "method": "GET", 549 | "headers": {}, 550 | })), 551 | &res(json!({"headers": { 552 | "cache-control": "public, max-age=999999" 553 | } 554 | })), 555 | ); 556 | 557 | assert_eq!(policy.is_stale(now), false); 558 | assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 999999); 559 | } 560 | 561 | #[test] 562 | fn test_weird_syntax() { 563 | let now = SystemTime::now(); 564 | let policy = CachePolicy::new( 565 | &req(json!({ 566 | "method": "GET", 567 | "headers": {}, 568 | })), 569 | &res(json!({"headers": { 570 | "cache-control": ",,,,max-age = 456 ," 571 | } 572 | })), 573 | ); 574 | 575 | assert_eq!(policy.is_stale(now), false); 576 | assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 456); 577 | 578 | #[cfg(feature = "serde")] 579 | { 580 | let json = serde_json::to_string(&policy).unwrap(); 581 | let policy: CachePolicy = serde_json::from_str(&json).unwrap(); 582 | 583 | assert_eq!(policy.is_stale(now), false); 584 | assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 456); 585 | } 586 | } 587 | 588 | #[test] 589 | fn test_quoted_syntax() { 590 | let now = SystemTime::now(); 591 | let policy = CachePolicy::new( 592 | &req(json!({ 593 | "method": "GET", 594 | "headers": {}, 595 | })), 596 | &res(json!({"headers": { 597 | "cache-control": " max-age = \"678\" " 598 | } 599 | })), 600 | ); 601 | 602 | assert_eq!(policy.is_stale(now), false); 603 | assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 678); 604 | } 605 | 606 | #[test] 607 | fn test_age_can_make_stale() { 608 | let now = SystemTime::now(); 609 | let policy = CachePolicy::new( 610 | &req(json!({ 611 | "method": "GET", 612 | "headers": {}, 613 | })), 614 | &res(json!({ 615 | "headers": { 616 | "cache-control": "max-age=100", 617 | "age": "101" 618 | } 619 | })), 620 | ); 621 | 622 | assert!(policy.is_stale(now)); 623 | assert!(policy.is_storable()); 624 | } 625 | 626 | #[test] 627 | fn test_age_not_always_stale() { 628 | let now = SystemTime::now(); 629 | let policy = CachePolicy::new( 630 | &req(json!({ 631 | "method": "GET", 632 | "headers": {}, 633 | })), 634 | &res(json!({ 635 | "headers": { 636 | "cache-control": "max-age=20", 637 | "age": "15" 638 | } 639 | })), 640 | ); 641 | 642 | assert_eq!(policy.is_stale(now), false); 643 | assert!(policy.is_storable()); 644 | } 645 | 646 | #[test] 647 | fn test_bogus_age_ignored() { 648 | let now = SystemTime::now(); 649 | let policy = CachePolicy::new( 650 | &req(json!({ 651 | "method": "GET", 652 | "headers": {}, 653 | })), 654 | &res(json!({ 655 | "headers": { 656 | "cache-control": "max-age=20", 657 | "age": "golden" 658 | } 659 | })), 660 | ); 661 | 662 | assert_eq!(policy.is_stale(now), false); 663 | assert!(policy.is_storable()); 664 | } 665 | 666 | #[test] 667 | fn test_immutable_simple_hit() { 668 | let now = SystemTime::now(); 669 | let policy = CachePolicy::new( 670 | &req(json!({ 671 | "method": "GET", 672 | "headers": {}, 673 | })), 674 | &res(json!({ 675 | "headers": { 676 | "cache-control": "immutable, max-age=999999", 677 | } 678 | })), 679 | ); 680 | 681 | assert_eq!(policy.is_stale(now), false); 682 | assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 999999); 683 | } 684 | 685 | #[test] 686 | fn test_immutable_can_expire() { 687 | let now = SystemTime::now(); 688 | let policy = CachePolicy::new( 689 | &req(json!({ 690 | "method": "GET", 691 | "headers": {}, 692 | })), 693 | &res(json!({ 694 | "headers": { 695 | "cache-control": "immutable, max-age=0", 696 | } 697 | })), 698 | ); 699 | 700 | assert!(policy.is_stale(now)); 701 | assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 0); 702 | } 703 | 704 | #[test] 705 | fn test_pragma_no_cache() { 706 | let now = SystemTime::now(); 707 | let policy = CachePolicy::new( 708 | &req(json!({ 709 | "method": "GET", 710 | "headers": {}, 711 | })), 712 | &res(json!({ 713 | "headers": { 714 | "pragma": "no-cache", 715 | "last-modified": "Mon, 07 Mar 2016 11:52:56 GMT", 716 | } 717 | })), 718 | ); 719 | 720 | assert!(policy.is_stale(now)); 721 | } 722 | 723 | #[test] 724 | fn test_no_store() { 725 | let now = SystemTime::now(); 726 | let policy = CachePolicy::new( 727 | &req(json!({ 728 | "method": "GET", 729 | "headers": {}, 730 | })), 731 | &res(json!({ 732 | "headers": { 733 | "cache-control": "no-store, public, max-age=1", 734 | } 735 | })), 736 | ); 737 | 738 | assert!(policy.is_stale(now)); 739 | assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 0); 740 | } 741 | 742 | #[test] 743 | fn test_observe_private_cache() { 744 | let now = SystemTime::now(); 745 | let private_header = json!({ 746 | "cache-control": "private, max-age=1234", 747 | }); 748 | 749 | let proxy_policy = CachePolicy::new( 750 | &req(json!({ 751 | "method": "GET", 752 | "headers": {}, 753 | })), 754 | &res(json!({ "headers": private_header })), 755 | ); 756 | 757 | assert!(proxy_policy.is_stale(now)); 758 | assert_eq!((proxy_policy.time_to_live(now) + proxy_policy.age(now)).as_secs(), 0); 759 | 760 | let ua_cache = CachePolicy::new_options( 761 | &req(json!({ 762 | "method": "GET", 763 | "headers": {}, 764 | })), 765 | &res(json!({ "headers": private_header })), 766 | now, 767 | CacheOptions { 768 | shared: false, 769 | ..Default::default() 770 | }, 771 | ); 772 | assert_eq!(ua_cache.is_stale(now), false); 773 | assert_eq!(ua_cache.time_to_live(now).as_secs(), 1234); 774 | } 775 | 776 | #[test] 777 | fn test_do_not_share_cookies() { 778 | let now = SystemTime::now(); 779 | let cookie_header = json!({ 780 | "set-cookie": "foo=bar", 781 | "cache-control": "max-age=99", 782 | }); 783 | 784 | let proxy_policy = CachePolicy::new_options( 785 | &req(json!({ 786 | "method": "GET", 787 | "headers": {}, 788 | })), 789 | &res(json!({ "headers": cookie_header })), 790 | now, 791 | CacheOptions { 792 | shared: true, 793 | ..Default::default() 794 | }, 795 | ); 796 | 797 | assert!(proxy_policy.is_stale(now)); 798 | assert_eq!((proxy_policy.time_to_live(now) + proxy_policy.age(now)).as_secs(), 0); 799 | 800 | let ua_cache = CachePolicy::new_options( 801 | &req(json!({ 802 | "method": "GET", 803 | "headers": {}, 804 | })), 805 | &res(json!({ "headers": cookie_header })), 806 | now, 807 | CacheOptions { 808 | shared: false, 809 | ..Default::default() 810 | }, 811 | ); 812 | assert_eq!(ua_cache.is_stale(now), false); 813 | assert_eq!(ua_cache.time_to_live(now).as_secs(), 99); 814 | } 815 | 816 | #[test] 817 | fn test_do_share_cookies_if_immutable() { 818 | let now = SystemTime::now(); 819 | let cookie_header = json!({ 820 | "set-cookie": "foo=bar", 821 | "cache-control": "immutable, max-age=99", 822 | }); 823 | 824 | let proxy_policy = CachePolicy::new_options( 825 | &req(json!({ 826 | "method": "GET", 827 | "headers": {}, 828 | })), 829 | &res(json!({ "headers": cookie_header })), 830 | now, 831 | CacheOptions { 832 | shared: true, 833 | ..Default::default() 834 | }, 835 | ); 836 | 837 | assert_eq!(proxy_policy.is_stale(now), false); 838 | assert_eq!((proxy_policy.time_to_live(now) + proxy_policy.age(now)).as_secs(), 99); 839 | } 840 | 841 | #[test] 842 | fn test_cache_explicitly_public_cookie() { 843 | let now = SystemTime::now(); 844 | let cookie_header = json!({ 845 | "set-cookie": "foo=bar", 846 | "cache-control": "max-age=5, public", 847 | }); 848 | 849 | let proxy_policy = CachePolicy::new_options( 850 | &req(json!({ 851 | "method": "GET", 852 | "headers": {}, 853 | })), 854 | &res(json!({ "headers": cookie_header })), 855 | now, 856 | CacheOptions { 857 | shared: true, 858 | ..Default::default() 859 | }, 860 | ); 861 | 862 | assert_eq!(proxy_policy.is_stale(now), false); 863 | assert_eq!((proxy_policy.time_to_live(now) + proxy_policy.age(now)).as_secs(), 5); 864 | } 865 | 866 | #[test] 867 | fn test_miss_max_age_equals_zero() { 868 | let now = SystemTime::now(); 869 | let policy = CachePolicy::new( 870 | &req(json!({ 871 | "method": "GET", 872 | "headers": {}, 873 | })), 874 | &res(json!({ 875 | "headers": { 876 | "cache-control": "public, max-age=0", 877 | }, 878 | })), 879 | ); 880 | 881 | assert!(policy.is_stale(now)); 882 | assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 0); 883 | } 884 | 885 | #[test] 886 | fn test_uncacheable_503() { 887 | let now = SystemTime::now(); 888 | let policy = CachePolicy::new( 889 | &req(json!({ 890 | "method": "GET", 891 | "headers": {}, 892 | })), 893 | &res(json!({ 894 | "status": 503, 895 | "headers": { 896 | "cache-control": "public, max-age=0", 897 | }, 898 | })), 899 | ); 900 | 901 | assert!(policy.is_stale(now)); 902 | assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 0); 903 | } 904 | 905 | #[test] 906 | fn test_cacheable_301() { 907 | let now = SystemTime::now(); 908 | let policy = CachePolicy::new( 909 | &req(json!({ 910 | "method": "GET", 911 | "headers": {}, 912 | })), 913 | &res(json!({ 914 | "status": 301, 915 | "headers": { 916 | "last-modified": "Mon, 07 Mar 2016 11:52:56 GMT", 917 | }, 918 | })), 919 | ); 920 | 921 | assert_eq!(policy.is_stale(now), false); 922 | } 923 | 924 | #[test] 925 | fn test_uncacheable_303() { 926 | let now = SystemTime::now(); 927 | let policy = CachePolicy::new( 928 | &req(json!({ 929 | "method": "GET", 930 | "headers": {}, 931 | })), 932 | &res(json!({ 933 | "status": 303, 934 | "headers": { 935 | "last-modified": "Mon, 07 Mar 2016 11:52:56 GMT", 936 | }, 937 | })), 938 | ); 939 | 940 | assert!(policy.is_stale(now)); 941 | assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 0); 942 | } 943 | 944 | #[test] 945 | fn test_cacheable_303() { 946 | let now = SystemTime::now(); 947 | let policy = CachePolicy::new( 948 | &req(json!({ 949 | "method": "GET", 950 | "headers": {}, 951 | })), 952 | &res(json!({ 953 | "status": 303, 954 | "headers": { 955 | "cache-control": "max-age=1000", 956 | }, 957 | })), 958 | ); 959 | 960 | assert_eq!(policy.is_stale(now), false); 961 | } 962 | 963 | #[test] 964 | fn test_uncacheable_412() { 965 | let now = SystemTime::now(); 966 | let policy = CachePolicy::new( 967 | &req(json!({ 968 | "method": "GET", 969 | "headers": {}, 970 | })), 971 | &res(json!({ 972 | "status": 412, 973 | "headers": { 974 | "cache-control": "public, max-age=1000", 975 | }, 976 | })), 977 | ); 978 | 979 | assert!(policy.is_stale(now)); 980 | assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 0); 981 | } 982 | 983 | #[test] 984 | fn test_expired_expires_cache_with_max_age() { 985 | let now = SystemTime::now(); 986 | let policy = CachePolicy::new( 987 | &req(json!({ 988 | "method": "GET", 989 | "headers": {}, 990 | })), 991 | &res(json!({ 992 | "headers": { 993 | "cache-control": "public, max-age=9999", 994 | "expires": "Sat, 07 May 2016 15:35:18 GMT", 995 | }, 996 | })), 997 | ); 998 | 999 | assert_eq!(policy.is_stale(now), false); 1000 | assert_eq!((policy.time_to_live(now) + policy.age(now)).as_secs(), 9999); 1001 | } 1002 | 1003 | #[test] 1004 | fn test_expired_expires_cached_with_s_maxage() { 1005 | let now = SystemTime::now(); 1006 | let s_max_age_headers = json!({ 1007 | "cache-control": "public, s-maxage=9999", 1008 | "expires": "Sat, 07 May 2016 15:35:18 GMT", 1009 | }); 1010 | 1011 | let proxy_policy = CachePolicy::new( 1012 | &req(json!({ 1013 | "method": "GET", 1014 | "headers": {}, 1015 | })), 1016 | &res(json!({ 1017 | "headers": s_max_age_headers, 1018 | })), 1019 | ); 1020 | 1021 | assert_eq!(proxy_policy.is_stale(now), false); 1022 | assert_eq!((proxy_policy.time_to_live(now) + proxy_policy.age(now)).as_secs(), 9999); 1023 | 1024 | let ua_policy = CachePolicy::new_options( 1025 | &req(json!({ 1026 | "method": "GET", 1027 | "headers": {}, 1028 | })), 1029 | &res(json!({ 1030 | "headers": s_max_age_headers, 1031 | })), 1032 | now, 1033 | CacheOptions { 1034 | shared: false, 1035 | ..Default::default() 1036 | }, 1037 | ); 1038 | assert!(ua_policy.is_stale(now)); 1039 | assert_eq!((ua_policy.time_to_live(now) + ua_policy.age(now)).as_secs(), 0); 1040 | } 1041 | 1042 | #[test] 1043 | fn test_when_urls_match() { 1044 | let now = SystemTime::now(); 1045 | let policy = CachePolicy::new( 1046 | &req(json!({ 1047 | "uri": "/", 1048 | "headers": {}, 1049 | })), 1050 | &res(json!({ 1051 | "status": 200, 1052 | "headers": { 1053 | "cache-control": "max-age=2", 1054 | }, 1055 | })), 1056 | ); 1057 | 1058 | assert!(policy 1059 | .before_request( 1060 | &req(json!({ 1061 | "uri": "/", 1062 | "headers": {}, 1063 | })), 1064 | now 1065 | ) 1066 | .satisfies_without_revalidation()); 1067 | } 1068 | 1069 | #[test] 1070 | fn test_not_when_urls_mismatch() { 1071 | let now = SystemTime::now(); 1072 | let policy = CachePolicy::new( 1073 | &req(json!({ 1074 | "uri": "/foo", 1075 | "headers": {}, 1076 | })), 1077 | &res(json!({ 1078 | "status": 200, 1079 | "headers": { 1080 | "cache-control": "max-age=2", 1081 | }, 1082 | })), 1083 | ); 1084 | 1085 | assert!(!policy 1086 | .before_request( 1087 | &req(json!({ 1088 | "uri": "/foo?bar", 1089 | "headers": {}, 1090 | })), 1091 | now 1092 | ) 1093 | .satisfies_without_revalidation()); 1094 | } 1095 | 1096 | #[test] 1097 | fn test_when_methods_match() { 1098 | let now = SystemTime::now(); 1099 | let policy = CachePolicy::new( 1100 | &req(json!({ 1101 | "method": "GET", 1102 | "headers": {}, 1103 | })), 1104 | &res(json!({ 1105 | "status": 200, 1106 | "headers": { 1107 | "cache-control": "max-age=2", 1108 | }, 1109 | })), 1110 | ); 1111 | 1112 | assert!( 1113 | policy 1114 | .before_request( 1115 | &req(json!({ 1116 | "method": "GET", 1117 | "headers": {}, 1118 | })), 1119 | now 1120 | ) 1121 | .satisfies_without_revalidation(), 1122 | "{policy:?}" 1123 | ); 1124 | } 1125 | 1126 | #[test] 1127 | fn test_not_when_hosts_mismatch() { 1128 | let now = SystemTime::now(); 1129 | let policy = CachePolicy::new( 1130 | &req(json!({ 1131 | "headers": { 1132 | "host": "foo", 1133 | }, 1134 | })), 1135 | &res(json!({ 1136 | "status": 200, 1137 | "headers": { 1138 | "cache-control": "max-age=2", 1139 | }, 1140 | })), 1141 | ); 1142 | 1143 | assert!(policy 1144 | .before_request( 1145 | &req(json!({ 1146 | "headers": { 1147 | "host": "foo", 1148 | }, 1149 | })), 1150 | now 1151 | ) 1152 | .satisfies_without_revalidation()); 1153 | 1154 | assert!(!policy 1155 | .before_request( 1156 | &req(json!({ 1157 | "headers": { 1158 | "host": "foofoo", 1159 | }, 1160 | })), 1161 | now 1162 | ) 1163 | .satisfies_without_revalidation()); 1164 | } 1165 | 1166 | #[test] 1167 | fn test_when_methods_match_head() { 1168 | let now = SystemTime::now(); 1169 | let policy = CachePolicy::new( 1170 | &req(json!({ 1171 | "method": "HEAD", 1172 | "headers": {}, 1173 | })), 1174 | &res(json!({ 1175 | "status": 200, 1176 | "headers": { 1177 | "cache-control": "max-age=2", 1178 | }, 1179 | })), 1180 | ); 1181 | 1182 | assert!(policy 1183 | .before_request( 1184 | &req(json!({ 1185 | "method": "HEAD", 1186 | "headers": {}, 1187 | })), 1188 | now 1189 | ) 1190 | .satisfies_without_revalidation()); 1191 | } 1192 | 1193 | #[test] 1194 | fn test_not_when_methods_mismatch() { 1195 | let now = SystemTime::now(); 1196 | let policy = CachePolicy::new( 1197 | &req(json!({ 1198 | "method": "POST", 1199 | "headers": {}, 1200 | })), 1201 | &res(json!({ 1202 | "status": 200, 1203 | "headers": { 1204 | "cache-control": "max-age=2", 1205 | }, 1206 | })), 1207 | ); 1208 | 1209 | assert!(!policy 1210 | .before_request( 1211 | &req(json!({ 1212 | "method": "GET", 1213 | "headers": {}, 1214 | })), 1215 | now 1216 | ) 1217 | .satisfies_without_revalidation()); 1218 | } 1219 | 1220 | #[test] 1221 | fn test_not_when_methods_mismatch_head() { 1222 | let now = SystemTime::now(); 1223 | let policy = CachePolicy::new( 1224 | &req(json!({ 1225 | "method": "HEAD", 1226 | "headers": {}, 1227 | })), 1228 | &res(json!({ 1229 | "status": 200, 1230 | "headers": { 1231 | "cache-control": "max-age=2", 1232 | }, 1233 | })), 1234 | ); 1235 | 1236 | assert_eq!( 1237 | policy 1238 | .before_request( 1239 | &req(json!({ 1240 | "method": "GET", 1241 | "headers": {}, 1242 | })), 1243 | now 1244 | ) 1245 | .satisfies_without_revalidation(), 1246 | false 1247 | ); 1248 | } 1249 | 1250 | #[test] 1251 | fn test_not_when_proxy_revalidating() { 1252 | let now = SystemTime::now(); 1253 | let policy = CachePolicy::new( 1254 | &req(json!({ 1255 | "headers": {}, 1256 | })), 1257 | &res(json!({ 1258 | "status": 200, 1259 | "headers": { 1260 | "cache-control": "max-age=2, proxy-revalidate ", 1261 | }, 1262 | })), 1263 | ); 1264 | 1265 | assert!(!policy 1266 | .before_request( 1267 | &req(json!({ 1268 | "headers": {}, 1269 | })), 1270 | now 1271 | ) 1272 | .satisfies_without_revalidation()); 1273 | } 1274 | 1275 | #[test] 1276 | fn test_when_not_a_proxy_revalidating() { 1277 | let now = SystemTime::now(); 1278 | let policy = CachePolicy::new_options( 1279 | &req(json!({ 1280 | "headers": {}, 1281 | })), 1282 | &res(json!({ 1283 | "status": 200, 1284 | "headers": { 1285 | "cache-control": "max-age=2, proxy-revalidate ", 1286 | }, 1287 | })), 1288 | now, 1289 | CacheOptions { 1290 | shared: false, 1291 | ..Default::default() 1292 | }, 1293 | ); 1294 | assert!(policy 1295 | .before_request( 1296 | &req(json!({ 1297 | "headers": {}, 1298 | })), 1299 | now 1300 | ) 1301 | .satisfies_without_revalidation()); 1302 | } 1303 | 1304 | #[test] 1305 | fn test_not_when_no_cache_requesting() { 1306 | let now = SystemTime::now(); 1307 | let policy = CachePolicy::new_options( 1308 | &req(json!({ 1309 | "headers": {}, 1310 | })), 1311 | &res(json!({ 1312 | "headers": { 1313 | "cache-control": "max-age=2", 1314 | }, 1315 | })), 1316 | now, 1317 | CacheOptions { 1318 | shared: false, 1319 | ..Default::default() 1320 | }, 1321 | ); 1322 | assert!(policy 1323 | .before_request( 1324 | &req(json!({ 1325 | "headers": { 1326 | "cache-control": "fine", 1327 | }, 1328 | })), 1329 | now 1330 | ) 1331 | .satisfies_without_revalidation()); 1332 | 1333 | assert_eq!( 1334 | policy 1335 | .before_request( 1336 | &req(json!({ 1337 | "headers": { 1338 | "cache-control": "no-cache", 1339 | }, 1340 | })), 1341 | now 1342 | ) 1343 | .satisfies_without_revalidation(), 1344 | false 1345 | ); 1346 | 1347 | assert_eq!( 1348 | policy 1349 | .before_request( 1350 | &req(json!({ 1351 | "headers": { 1352 | "cache-control": "no-cache", 1353 | }, 1354 | })), 1355 | now 1356 | ) 1357 | .satisfies_without_revalidation(), 1358 | false 1359 | ); 1360 | } 1361 | 1362 | #[test] 1363 | fn test_vary_basic() { 1364 | let now = SystemTime::now(); 1365 | let policy = CachePolicy::new( 1366 | &req(json!({ 1367 | "headers": { 1368 | "weather": "nice", 1369 | }, 1370 | })), 1371 | &res(json!({ 1372 | "headers": { 1373 | "cache-control": "max-age=5", 1374 | "vary": "weather", 1375 | }, 1376 | })), 1377 | ); 1378 | 1379 | assert!(policy 1380 | .before_request( 1381 | &req(json!({ 1382 | "headers": { 1383 | "weather": "nice", 1384 | }, 1385 | })), 1386 | now 1387 | ) 1388 | .satisfies_without_revalidation()); 1389 | 1390 | assert_eq!( 1391 | policy 1392 | .before_request( 1393 | &req(json!({ 1394 | "headers": { 1395 | "weather": "bad", 1396 | }, 1397 | })), 1398 | now 1399 | ) 1400 | .satisfies_without_revalidation(), 1401 | false 1402 | ); 1403 | } 1404 | 1405 | #[test] 1406 | fn test_asterisks_does_not_match() { 1407 | let now = SystemTime::now(); 1408 | let policy = CachePolicy::new( 1409 | &req(json!({ 1410 | "headers": { 1411 | "weather": "ok", 1412 | }, 1413 | })), 1414 | &res(json!({ 1415 | "headers": { 1416 | "cache-control": "max-age=5", 1417 | "vary": "*", 1418 | }, 1419 | })), 1420 | ); 1421 | 1422 | assert_eq!( 1423 | policy 1424 | .before_request( 1425 | &req(json!({ 1426 | "headers": { 1427 | "weather": "ok", 1428 | }, 1429 | })), 1430 | now 1431 | ) 1432 | .satisfies_without_revalidation(), 1433 | false 1434 | ); 1435 | } 1436 | 1437 | #[test] 1438 | fn test_asterisks_is_stale() { 1439 | let now = SystemTime::now(); 1440 | let policy_one = CachePolicy::new( 1441 | &req(json!({ 1442 | "headers": { 1443 | "weather": "ok", 1444 | }, 1445 | })), 1446 | &res(json!({ 1447 | "headers": { 1448 | "cache-control": "public,max-age=99", 1449 | "vary": "*", 1450 | }, 1451 | })), 1452 | ); 1453 | 1454 | let policy_two = CachePolicy::new( 1455 | &req(json!({ 1456 | "headers": { 1457 | "weather": "ok", 1458 | }, 1459 | })), 1460 | &res(json!({ 1461 | "headers": { 1462 | "cache-control": "public,max-age=99", 1463 | "vary": "weather", 1464 | }, 1465 | })), 1466 | ); 1467 | 1468 | assert!(policy_one.is_stale(now)); 1469 | assert_eq!(policy_two.is_stale(now), false); 1470 | } 1471 | 1472 | #[test] 1473 | fn test_values_are_case_sensitive() { 1474 | let now = SystemTime::now(); 1475 | let policy = CachePolicy::new( 1476 | &req(json!({ 1477 | "headers": { 1478 | "weather": "BAD", 1479 | }, 1480 | })), 1481 | &res(json!({ 1482 | "headers": { 1483 | "cache-control": "public,max-age=5", 1484 | "vary": "Weather", 1485 | }, 1486 | })), 1487 | ); 1488 | 1489 | assert!(policy 1490 | .before_request( 1491 | &req(json!({ 1492 | "headers": { 1493 | "weather": "BAD", 1494 | }, 1495 | })), 1496 | now 1497 | ) 1498 | .satisfies_without_revalidation()); 1499 | 1500 | assert_eq!( 1501 | policy 1502 | .before_request( 1503 | &req(json!({ 1504 | "headers": { 1505 | "weather": "bad", 1506 | }, 1507 | })), 1508 | now 1509 | ) 1510 | .satisfies_without_revalidation(), 1511 | false 1512 | ); 1513 | } 1514 | 1515 | #[test] 1516 | fn test_irrelevant_headers_ignored() { 1517 | let now = SystemTime::now(); 1518 | let policy = CachePolicy::new( 1519 | &req(json!({ 1520 | "headers": { 1521 | "weather": "nice", 1522 | }, 1523 | })), 1524 | &res(json!({ 1525 | "headers": { 1526 | "cache-control": "max-age=5", 1527 | "vary": "moon-phase", 1528 | }, 1529 | })), 1530 | ); 1531 | 1532 | assert!(policy 1533 | .before_request( 1534 | &req(json!({ 1535 | "headers": { 1536 | "weather": "bad", 1537 | }, 1538 | })), 1539 | now 1540 | ) 1541 | .satisfies_without_revalidation()); 1542 | 1543 | assert!(policy 1544 | .before_request( 1545 | &req(json!({ 1546 | "headers": { 1547 | "weather": "shining", 1548 | }, 1549 | })), 1550 | now 1551 | ) 1552 | .satisfies_without_revalidation()); 1553 | 1554 | assert_eq!( 1555 | policy 1556 | .before_request( 1557 | &req(json!({ 1558 | "headers": { 1559 | "moon-phase": "full", 1560 | }, 1561 | })), 1562 | now 1563 | ) 1564 | .satisfies_without_revalidation(), 1565 | false 1566 | ); 1567 | } 1568 | 1569 | #[test] 1570 | fn test_absence_is_meaningful() { 1571 | let now = SystemTime::now(); 1572 | let policy = CachePolicy::new( 1573 | &req(json!({ 1574 | "headers": { 1575 | "weather": "nice", 1576 | }, 1577 | })), 1578 | &res(json!({ 1579 | "headers": { 1580 | "cache-control": "max-age=5", 1581 | "vary": "moon-phase, weather", 1582 | }, 1583 | })), 1584 | ); 1585 | 1586 | assert!(policy 1587 | .before_request( 1588 | &req(json!({ 1589 | "headers": { 1590 | "weather": "nice", 1591 | }, 1592 | })), 1593 | now 1594 | ) 1595 | .satisfies_without_revalidation()); 1596 | 1597 | assert_eq!( 1598 | policy 1599 | .before_request( 1600 | &req(json!({ 1601 | "headers": { 1602 | "weather": "nice", 1603 | "moon-phase": "", 1604 | }, 1605 | })), 1606 | now 1607 | ) 1608 | .satisfies_without_revalidation(), 1609 | false 1610 | ); 1611 | 1612 | assert_eq!( 1613 | policy 1614 | .before_request( 1615 | &req(json!({ 1616 | "headers": {}, 1617 | })), 1618 | now 1619 | ) 1620 | .satisfies_without_revalidation(), 1621 | false 1622 | ); 1623 | } 1624 | 1625 | #[test] 1626 | fn test_all_values_must_match() { 1627 | let now = SystemTime::now(); 1628 | let policy = CachePolicy::new( 1629 | &req(json!({ 1630 | "headers": { 1631 | "sun": "shining", 1632 | "weather": "nice", 1633 | }, 1634 | })), 1635 | &res(json!({ 1636 | "headers": { 1637 | "cache-control": "max-age=5", 1638 | "vary": "weather, sun", 1639 | }, 1640 | })), 1641 | ); 1642 | 1643 | assert!(policy 1644 | .before_request( 1645 | &req(json!({ 1646 | "headers": { 1647 | "sun": "shining", 1648 | "weather": "nice", 1649 | }, 1650 | })), 1651 | now 1652 | ) 1653 | .satisfies_without_revalidation()); 1654 | 1655 | assert_eq!( 1656 | policy 1657 | .before_request( 1658 | &req(json!({ 1659 | "headers": { 1660 | "sun": "shining", 1661 | "weather": "bad", 1662 | }, 1663 | })), 1664 | now 1665 | ) 1666 | .satisfies_without_revalidation(), 1667 | false 1668 | ); 1669 | } 1670 | 1671 | #[test] 1672 | fn test_whitespace_is_okay() { 1673 | let now = SystemTime::now(); 1674 | let policy = CachePolicy::new( 1675 | &req(json!({ 1676 | "headers": { 1677 | "sun": "shining", 1678 | "weather": "nice", 1679 | }, 1680 | })), 1681 | &res(json!({ 1682 | "headers": { 1683 | "cache-control": "max-age=5", 1684 | "vary": " weather , sun ", 1685 | }, 1686 | })), 1687 | ); 1688 | 1689 | assert!(policy 1690 | .before_request( 1691 | &req(json!({ 1692 | "headers": { 1693 | "sun": "shining", 1694 | "weather": "nice", 1695 | }, 1696 | })), 1697 | now 1698 | ) 1699 | .satisfies_without_revalidation()); 1700 | 1701 | assert_eq!( 1702 | policy 1703 | .before_request( 1704 | &req(json!({ 1705 | "headers": { 1706 | "weather": "nice", 1707 | }, 1708 | })), 1709 | now 1710 | ) 1711 | .satisfies_without_revalidation(), 1712 | false 1713 | ); 1714 | 1715 | assert_eq!( 1716 | policy 1717 | .before_request( 1718 | &req(json!({ 1719 | "headers": { 1720 | "sun": "shining", 1721 | }, 1722 | })), 1723 | now 1724 | ) 1725 | .satisfies_without_revalidation(), 1726 | false 1727 | ); 1728 | } 1729 | 1730 | #[test] 1731 | fn test_order_is_irrelevant() { 1732 | let now = SystemTime::now(); 1733 | let policy_one = CachePolicy::new( 1734 | &req(json!({ 1735 | "headers": { 1736 | "sun": "shining", 1737 | "weather": "nice", 1738 | }, 1739 | })), 1740 | &res(json!({ 1741 | "headers": { 1742 | "cache-control": "max-age=5", 1743 | "vary": "weather, sun", 1744 | }, 1745 | })), 1746 | ); 1747 | 1748 | let policy_two = CachePolicy::new( 1749 | &req(json!({ 1750 | "headers": { 1751 | "sun": "shining", 1752 | "weather": "nice", 1753 | }, 1754 | })), 1755 | &res(json!({ 1756 | "headers": { 1757 | "cache-control": "max-age=5", 1758 | "vary": "sun, weather", 1759 | }, 1760 | })), 1761 | ); 1762 | 1763 | assert!(policy_one 1764 | .before_request( 1765 | &req(json!({ 1766 | "headers": { 1767 | "weather": "nice", 1768 | "sun": "shining", 1769 | }, 1770 | })), 1771 | now 1772 | ) 1773 | .satisfies_without_revalidation()); 1774 | 1775 | assert!(policy_one 1776 | .before_request( 1777 | &req(json!({ 1778 | "headers": { 1779 | "sun": "shining", 1780 | "weather": "nice", 1781 | }, 1782 | })), 1783 | now 1784 | ) 1785 | .satisfies_without_revalidation()); 1786 | 1787 | assert!(policy_two 1788 | .before_request( 1789 | &req(json!({ 1790 | "headers": { 1791 | "weather": "nice", 1792 | "sun": "shining", 1793 | }, 1794 | })), 1795 | now 1796 | ) 1797 | .satisfies_without_revalidation()); 1798 | 1799 | assert!(policy_two 1800 | .before_request( 1801 | &req(json!({ 1802 | "headers": { 1803 | "sun": "shining", 1804 | "weather": "nice", 1805 | }, 1806 | })), 1807 | now 1808 | ) 1809 | .satisfies_without_revalidation()); 1810 | } 1811 | --------------------------------------------------------------------------------