worker/cache.rs
1use std::convert::TryInto;
2
3use serde::Serialize;
4use wasm_bindgen::JsCast;
5use wasm_bindgen_futures::JsFuture;
6use worker_sys::ext::CacheStorageExt;
7
8use crate::request::Request;
9use crate::response::Response;
10use crate::Result;
11
12/// Provides access to the [Cache API](https://developers.cloudflare.com/workers/runtime-apis/cache).
13/// Because `match` is a reserved keyword in Rust, the `match` method has been renamed to `get`.
14///
15/// Our implementation of the Cache API respects the following HTTP headers on the response passed to `put()`:
16///
17/// - `Cache-Control`
18/// - Controls caching directives.
19/// This is consistent with [Cloudflare Cache-Control Directives](https://developers.cloudflare.com/cache/about/cache-control#cache-control-directives).
20/// Refer to [Edge TTL](https://developers.cloudflare.com/cache/how-to/configure-cache-status-code#edge-ttl) for a list of HTTP response codes and their TTL when Cache-Control directives are not present.
21/// - `Cache-Tag`
22/// - Allows resource purging by tag(s) later (Enterprise only).
23/// - `ETag`
24/// - Allows `cache.get()` to evaluate conditional requests with If-None-Match.
25/// - `Expires`
26/// - A string that specifies when the resource becomes invalid.
27/// - `Last-Modified`
28/// - Allows `cache.get()` to evaluate conditional requests with If-Modified-Since.
29///
30/// This differs from the web browser Cache API as they do not honor any headers on the request or response.
31///
32/// Responses with `Set-Cookie` headers are never cached, because this sometimes indicates that the response contains unique data. To store a response with a `Set-Cookie` header, either delete that header or set `Cache-Control: private=Set-Cookie` on the response before calling `cache.put()`.
33///
34/// Use the `Cache-Control` method to store the response without the `Set-Cookie` header.
35#[derive(Debug)]
36pub struct Cache {
37 inner: web_sys::Cache,
38}
39
40impl Default for Cache {
41 fn default() -> Self {
42 let global: web_sys::WorkerGlobalScope = js_sys::global().unchecked_into();
43
44 Self {
45 inner: global.caches().unwrap().default(),
46 }
47 }
48}
49
50impl Cache {
51 /// Opens a [`Cache`] by name. To access the default global cache, use [`Cache::default()`](`Default::default`).
52 pub async fn open(name: String) -> Self {
53 let global: web_sys::WorkerGlobalScope = js_sys::global().unchecked_into();
54 let cache = global.caches().unwrap().open(&name);
55
56 // unwrap is safe because this promise never rejects
57 // https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage/open
58 let inner = JsFuture::from(cache).await.unwrap().into();
59
60 Self { inner }
61 }
62
63 /// Adds to the cache a [`Response`] keyed to the given request.
64 ///
65 /// The [`Response`] should include a `cache-control` header with `max-age` or `s-maxage` directives,
66 /// otherwise the Cache API will not cache the response.
67 /// The `stale-while-revalidate` and `stale-if-error` directives are not supported
68 /// when using the `cache.put` or `cache.get` methods.
69 /// For more information about how the Cache works, visit the documentation at [Cache API](https://developers.cloudflare.com/workers/runtime-apis/cache/)
70 /// and [Cloudflare Cache-Control Directives](https://developers.cloudflare.com/cache/about/cache-control#cache-control-directives)
71 ///
72 /// The Cache API will throw an error if:
73 /// - the request passed is a method other than GET.
74 /// - the response passed has a status of 206 Partial Content.
75 /// - the response passed contains the header `Vary: *` (required by the Cache API specification).
76 pub async fn put<'a, K: Into<CacheKey<'a>>>(&self, key: K, response: Response) -> Result<()> {
77 let promise = match key.into() {
78 CacheKey::Url(url) => self.inner.put_with_str(url.as_str(), &response.into()),
79 CacheKey::Request(request) => self
80 .inner
81 .put_with_request(&request.try_into()?, &response.into()),
82 };
83 let _ = JsFuture::from(promise).await?;
84 Ok(())
85 }
86
87 /// Returns the [`Response`] object keyed to that request. Never sends a subrequest to the origin. If no matching response is found in cache, returns `None`.
88 ///
89 /// Unlike the browser Cache API, Cloudflare Workers do not support the `ignoreSearch` or `ignoreVary` options on `get()`. You can accomplish this behavior by removing query strings or HTTP headers at `put()` time.
90 /// In addition, the `stale-while-revalidate` and `stale-if-error` directives are not supported
91 /// when using the `cache.put` or `cache.get` methods.
92 ///
93 /// Our implementation of the Cache API respects the following HTTP headers on the request passed to `get()`:
94 ///
95 /// - Range
96 /// - Results in a `206` response if a matching response with a Content-Length header is found. Your Cloudflare cache always respects range requests, even if an `Accept-Ranges` header is on the response.
97 /// - If-Modified-Since
98 /// - Results in a `304` response if a matching response is found with a `Last-Modified` header with a value after the time specified in `If-Modified-Since`.
99 /// - If-None-Match
100 /// - Results in a `304` response if a matching response is found with an `ETag` header with a value that matches a value in `If-None-Match.`
101 pub async fn get<'a, K: Into<CacheKey<'a>>>(
102 &self,
103 key: K,
104 ignore_method: bool,
105 ) -> Result<Option<Response>> {
106 let options = web_sys::CacheQueryOptions::new();
107 options.set_ignore_method(ignore_method);
108
109 let promise = match key.into() {
110 CacheKey::Url(url) => self
111 .inner
112 .match_with_str_and_options(url.as_str(), &options),
113 CacheKey::Request(request) => self
114 .inner
115 .match_with_request_and_options(&request.try_into()?, &options),
116 };
117
118 // `match` returns either a response or undefined
119 let result = JsFuture::from(promise).await?;
120 if result.is_undefined() {
121 Ok(None)
122 } else {
123 let edge_response: web_sys::Response = result.into();
124 let response = Response::from(edge_response);
125 Ok(Some(response))
126 }
127 }
128
129 /// Deletes the [`Response`] object associated with the key.
130 ///
131 /// Returns:
132 /// - Success, if the response was cached but is now deleted
133 /// - ResponseNotFound, if the response was not in the cache at the time of deletion
134 pub async fn delete<'a, K: Into<CacheKey<'a>>>(
135 &self,
136 key: K,
137 ignore_method: bool,
138 ) -> Result<CacheDeletionOutcome> {
139 let options = web_sys::CacheQueryOptions::new();
140 options.set_ignore_method(ignore_method);
141
142 let promise = match key.into() {
143 CacheKey::Url(url) => self
144 .inner
145 .delete_with_str_and_options(url.as_str(), &options),
146 CacheKey::Request(request) => self
147 .inner
148 .delete_with_request_and_options(&request.try_into()?, &options),
149 };
150 let result = JsFuture::from(promise).await?;
151
152 // Unwrap is safe because we know this is a boolean
153 // https://developers.cloudflare.com/workers/runtime-apis/cache#delete
154 if result.as_bool().unwrap() {
155 Ok(CacheDeletionOutcome::Success)
156 } else {
157 Ok(CacheDeletionOutcome::ResponseNotFound)
158 }
159 }
160}
161
162/// The `String` or `Request` object used as the lookup key. `String`s are interpreted as the URL for a new `Request` object.
163#[derive(Debug)]
164pub enum CacheKey<'a> {
165 Url(String),
166 Request(&'a Request),
167}
168
169impl From<&str> for CacheKey<'_> {
170 fn from(url: &str) -> Self {
171 Self::Url(url.to_string())
172 }
173}
174
175impl From<String> for CacheKey<'_> {
176 fn from(url: String) -> Self {
177 Self::Url(url)
178 }
179}
180
181impl From<&String> for CacheKey<'_> {
182 fn from(url: &String) -> Self {
183 Self::Url(url.clone())
184 }
185}
186
187impl<'a> From<&'a Request> for CacheKey<'a> {
188 fn from(request: &'a Request) -> Self {
189 Self::Request(request)
190 }
191}
192
193/// Successful outcomes when attempting to delete a `Response` from the cache
194#[derive(Serialize, Debug, Clone)]
195pub enum CacheDeletionOutcome {
196 /// The response was cached but is now deleted
197 Success,
198 /// The response was not in the cache at the time of deletion.
199 ResponseNotFound,
200}