Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

An initial implementation of the cache API #428

Merged
merged 3 commits into from
Nov 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion packages/tre/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"exit-hook": "^2.2.1",
"get-port": "^5.1.1",
"glob-to-regexp": "^0.4.1",
"http-cache-semantics": "^4.1.0",
"kleur": "^4.1.5",
"stoppable": "^1.1.0",
"undici": "^5.10.0",
Expand All @@ -51,6 +52,7 @@
"@types/debug": "^4.1.7",
"@types/estree": "^1.0.0",
"@types/glob-to-regexp": "^0.4.1",
"@types/stoppable": "^1.1.1"
"@types/stoppable": "^1.1.1",
"@types/http-cache-semantics": "^4.0.1"
}
}
42 changes: 42 additions & 0 deletions packages/tre/src/plugins/cache/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# The Cache API
penalosa marked this conversation as resolved.
Show resolved Hide resolved
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:

## .add()
Unimplemented in the runtime

## .addAll()
Unimplemented in the runtime

## .match()
`workerd` guarantees:
- The method will always be `GET`
- The request headers will include `Cache-Control: only-if-cached` (which Miniflare ignores)
- The request headers will include `Cf-Cache-Namespaces` if this is a namespaced cache (i.e. `caches.open(...)`)
`workerd` expects:
- The `Cf-Cache-Status` header to be present with the value:
- `MISS` if it's a cache miss, in which case the rest of the response is ignored by `workerd`
- `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

## .put()
`workerd` guarantees:
- The method will always be `PUT`, and the cache key method will always be `GET`
- The headers will be the headers of the cache key, and the URL will be the URL of the cache key
- The headers will include `Cf-Cache-Namespaces` if this is a namespaced cache (i.e. `caches.open(...)`)
- The body contains the serialised response for storage
- The serialised response will never:
- Have a `206` status code
- Have a `Vary: *` header
- Have a `304` status code
`workerd` expects:
- A `204` (success) or `413` (failure) response code. It doesn't do anything with either

## .delete()
`workerd` guarantees:
- The method will always be `PURGE`, and the cache key method will always be `GET`
- The headers will include `Cf-Cache-Namespaces` if this is a namespaced cache (i.e. `caches.open(...)`)
- The header `X-Real-IP` will be set to `127.0.0.1`
- The remaining headers will be the cache key headers
`workerd` expects:
- Status `200` on success
- Status `404` on failure
- Status `429` on rate limit (which will throw in the user worker)
65 changes: 65 additions & 0 deletions packages/tre/src/plugins/cache/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { HeadersInit, Response } from "undici";
import { CfHeader } from "../shared/constants";

enum Status {
PayloadTooLarge = 413,
NotFound = 404,
CacheMiss = 504,
}

export async function fallible<T>(promise: Promise<T>): Promise<T | Response> {
try {
return await promise;
} catch (e) {
if (e instanceof CacheError) {
return e.toResponse();
}
throw e;
}
}

export class CacheError extends Error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should do this for R2Error too, but this should probably extend MiniflareError (or HttpError?), and use code instead of status. MiniflareError will automatically set the name correctly too based off new.target.

Copy link
Contributor Author

@penalosa penalosa Nov 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I copied this over from R2Error, but the reason it uses status rather than code is because it is the http status code. R2Error has another property v4Code (or v4code depending on context, the API is maddeningly inconsistent), so using code there would be a bit confusing, I think. For consistency, it might make sense to keep this as status. I'll have a look at extending from MiniflareError though

constructor(
private status: number,
message: string,
readonly headers: HeadersInit = []
) {
super(message);
this.name = "CacheError";
}

toResponse() {
return new Response(null, {
status: this.status,
headers: this.headers,
});
}

context(info: string) {
this.message += ` (${info})`;
return this;
}
}

export class StorageFailure extends CacheError {
constructor() {
super(Status.PayloadTooLarge, "Cache storage failed");
}
}

export class PurgeFailure extends CacheError {
constructor() {
super(Status.NotFound, "Couldn't find asset to purge");
}
}

export class CacheMiss extends CacheError {
constructor() {
super(
// workerd ignores this, but it's the correct status code
Status.CacheMiss,
"Asset not found in cache",
[[CfHeader.CacheStatus, "MISS"]]
);
}
}
Loading