Skip to content

Commit

Permalink
Add support for If-None-Match to Cache#match, closes #246
Browse files Browse the repository at this point in the history
  • Loading branch information
mrbbot committed Sep 13, 2022
1 parent 38b821d commit 932b537
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 6 deletions.
38 changes: 32 additions & 6 deletions packages/cache/src/cache.ts
Expand Up @@ -110,6 +110,15 @@ function getExpirationTtl(
}
}

// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag#syntax
const etagRegexp = /^(W\/)?"(.+)"$/;
export function parseETag(value: string): string | undefined {
// As we only use this for `If-None-Match` handling, which always use the weak
// comparison algorithm, ignore "W/" directives:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match
return etagRegexp.exec(value.trim())?.[2] ?? undefined;
}

// https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.1.1
const utcDateRegexp =
/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d\d (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d\d\d\d \d\d:\d\d:\d\d GMT$/;
Expand All @@ -123,22 +132,39 @@ function getMatchResponse(
resHeaders: Headers,
resBody: Uint8Array
): Response {
// If `If-Modified-Since` is set, perform a conditional request
// If `If-None-Match` is set, perform a conditional request:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match
const reqIfNoneMatchHeader = reqHeaders.get("If-None-Match");
const resETagHeader = resHeaders.get("ETag");
if (reqIfNoneMatchHeader !== null && resETagHeader !== null) {
const resETag = parseETag(resETagHeader);
if (resETag !== undefined) {
if (reqIfNoneMatchHeader.trim() === "*") {
return new Response(null, { status: 304, headers: resHeaders });
}
for (const reqIfNoneMatch of reqIfNoneMatchHeader.split(",")) {
if (resETag === parseETag(reqIfNoneMatch)) {
return new Response(null, { status: 304, headers: resHeaders });
}
}
}
}

// If `If-Modified-Since` is set, perform a conditional request:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since
const reqIfModifiedSinceHeader = reqHeaders.get("If-Modified-Since");
const resLastModifiedHeader = resHeaders.get("Last-Modified");
if (reqIfModifiedSinceHeader !== null && resLastModifiedHeader !== null) {
const reqIfModifiedSince = parseUTCDate(reqIfModifiedSinceHeader);
const resLastModified = parseUTCDate(resLastModifiedHeader);
// Comparison of NaN's (invalid dates), will always result in `false`
if (resLastModified <= reqIfModifiedSince) {
return new Response(null, {
status: 304, // Not Modified
headers: resHeaders,
});
return new Response(null, { status: 304, headers: resHeaders });
}
}

// If `Range` is set, return a partial response
// If `Range` is set, return a partial response:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range
const reqRangeHeader = reqHeaders.get("Range");
if (reqRangeHeader !== null) {
return _getRangeResponse(reqRangeHeader, resStatus, resHeaders, resBody);
Expand Down
31 changes: 31 additions & 0 deletions packages/cache/test/cache.spec.ts
Expand Up @@ -238,6 +238,37 @@ test("Cache: match throws if attempting to load cached response created with Min
"load cached data created with Miniflare 1 and must delete it.",
});
});
test("Cache: match respects If-None-Match header", async (t) => {
const { cache } = t.context;
const res = new Response("value", {
headers: {
ETag: '"thing"',
"Cache-Control": "max-age=3600",
},
});
await cache.put("http://localhost:8787/test", res);

const ifNoneMatch = (value: string) =>
new BaseRequest("http://localhost:8787/test", {
headers: { "If-None-Match": value },
});

// Check returns 304 only if an ETag in `If-Modified-Since` matches
let cacheRes = await cache.match(ifNoneMatch('"thing"'));
t.is(cacheRes?.status, 304);
cacheRes = await cache.match(ifNoneMatch(' W/"thing" '));
t.is(cacheRes?.status, 304);
cacheRes = await cache.match(ifNoneMatch('"not the thing"'));
t.is(cacheRes?.status, 200);
cacheRes = await cache.match(
ifNoneMatch('"not the thing", "thing" , W/"still not the thing"')
);
t.is(cacheRes?.status, 304);
cacheRes = await cache.match(ifNoneMatch("*"));
t.is(cacheRes?.status, 304);
cacheRes = await cache.match(ifNoneMatch(" * "));
t.is(cacheRes?.status, 304);
});
test("Cache: match respects If-Modified-Since header", async (t) => {
const { cache } = t.context;
const res = new Response("value", {
Expand Down

0 comments on commit 932b537

Please sign in to comment.