Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit be72411

Browse files
authored
An initial implementation of the cache API (#428)
* Initial cache implementation * Address PR comments * Address comments
1 parent 2f1c201 commit be72411

File tree

16 files changed

+626
-32
lines changed

16 files changed

+626
-32
lines changed

package-lock.json

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/tre/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"exit-hook": "^2.2.1",
4242
"get-port": "^5.1.1",
4343
"glob-to-regexp": "^0.4.1",
44+
"http-cache-semantics": "^4.1.0",
4445
"kleur": "^4.1.5",
4546
"stoppable": "^1.1.0",
4647
"undici": "^5.10.0",
@@ -51,6 +52,7 @@
5152
"@types/debug": "^4.1.7",
5253
"@types/estree": "^1.0.0",
5354
"@types/glob-to-regexp": "^0.4.1",
54-
"@types/stoppable": "^1.1.1"
55+
"@types/stoppable": "^1.1.1",
56+
"@types/http-cache-semantics": "^4.0.1"
5557
}
5658
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# The Cache API
2+
The Cache API within `workerd` is _extremely_ lenient with errors, and will work with a service that doesn't fully support the expected API. This is what it _should_ behave like:
3+
4+
## .add()
5+
Unimplemented in the runtime
6+
7+
## .addAll()
8+
Unimplemented in the runtime
9+
10+
## .match()
11+
`workerd` guarantees:
12+
- The method will always be `GET`
13+
- The request headers will include `Cache-Control: only-if-cached` (which Miniflare ignores)
14+
- The request headers will include `Cf-Cache-Namespaces` if this is a namespaced cache (i.e. `caches.open(...)`)
15+
`workerd` expects:
16+
- The `Cf-Cache-Status` header to be present with the value:
17+
- `MISS` if it's a cache miss, in which case the rest of the response is ignored by `workerd`
18+
- `HIT` if it's a cache hit, in which case `workerd` sends the response on to the user, including the full headers and full body
19+
20+
## .put()
21+
`workerd` guarantees:
22+
- The method will always be `PUT`, and the cache key method will always be `GET`
23+
- The headers will be the headers of the cache key, and the URL will be the URL of the cache key
24+
- The headers will include `Cf-Cache-Namespaces` if this is a namespaced cache (i.e. `caches.open(...)`)
25+
- The body contains the serialised response for storage
26+
- The serialised response will never:
27+
- Have a `206` status code
28+
- Have a `Vary: *` header
29+
- Have a `304` status code
30+
`workerd` expects:
31+
- A `204` (success) or `413` (failure) response code. It doesn't do anything with either
32+
33+
## .delete()
34+
`workerd` guarantees:
35+
- The method will always be `PURGE`, and the cache key method will always be `GET`
36+
- The headers will include `Cf-Cache-Namespaces` if this is a namespaced cache (i.e. `caches.open(...)`)
37+
- The header `X-Real-IP` will be set to `127.0.0.1`
38+
- The remaining headers will be the cache key headers
39+
`workerd` expects:
40+
- Status `200` on success
41+
- Status `404` on failure
42+
- Status `429` on rate limit (which will throw in the user worker)
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { HeadersInit, Response } from "undici";
2+
import { CfHeader } from "../shared/constants";
3+
4+
enum Status {
5+
PayloadTooLarge = 413,
6+
NotFound = 404,
7+
CacheMiss = 504,
8+
}
9+
10+
export async function fallible<T>(promise: Promise<T>): Promise<T | Response> {
11+
try {
12+
return await promise;
13+
} catch (e) {
14+
if (e instanceof CacheError) {
15+
return e.toResponse();
16+
}
17+
throw e;
18+
}
19+
}
20+
21+
export class CacheError extends Error {
22+
constructor(
23+
private status: number,
24+
message: string,
25+
readonly headers: HeadersInit = []
26+
) {
27+
super(message);
28+
this.name = "CacheError";
29+
}
30+
31+
toResponse() {
32+
return new Response(null, {
33+
status: this.status,
34+
headers: this.headers,
35+
});
36+
}
37+
38+
context(info: string) {
39+
this.message += ` (${info})`;
40+
return this;
41+
}
42+
}
43+
44+
export class StorageFailure extends CacheError {
45+
constructor() {
46+
super(Status.PayloadTooLarge, "Cache storage failed");
47+
}
48+
}
49+
50+
export class PurgeFailure extends CacheError {
51+
constructor() {
52+
super(Status.NotFound, "Couldn't find asset to purge");
53+
}
54+
}
55+
56+
export class CacheMiss extends CacheError {
57+
constructor() {
58+
super(
59+
// workerd ignores this, but it's the correct status code
60+
Status.CacheMiss,
61+
"Asset not found in cache",
62+
[[CfHeader.CacheStatus, "MISS"]]
63+
);
64+
}
65+
}

0 commit comments

Comments
 (0)