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

Commit 932b537

Browse files
committed
Add support for If-None-Match to Cache#match, closes #246
1 parent 38b821d commit 932b537

File tree

2 files changed

+63
-6
lines changed

2 files changed

+63
-6
lines changed

packages/cache/src/cache.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,15 @@ function getExpirationTtl(
110110
}
111111
}
112112

113+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag#syntax
114+
const etagRegexp = /^(W\/)?"(.+)"$/;
115+
export function parseETag(value: string): string | undefined {
116+
// As we only use this for `If-None-Match` handling, which always use the weak
117+
// comparison algorithm, ignore "W/" directives:
118+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match
119+
return etagRegexp.exec(value.trim())?.[2] ?? undefined;
120+
}
121+
113122
// https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.1.1
114123
const utcDateRegexp =
115124
/^(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$/;
@@ -123,22 +132,39 @@ function getMatchResponse(
123132
resHeaders: Headers,
124133
resBody: Uint8Array
125134
): Response {
126-
// If `If-Modified-Since` is set, perform a conditional request
135+
// If `If-None-Match` is set, perform a conditional request:
136+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match
137+
const reqIfNoneMatchHeader = reqHeaders.get("If-None-Match");
138+
const resETagHeader = resHeaders.get("ETag");
139+
if (reqIfNoneMatchHeader !== null && resETagHeader !== null) {
140+
const resETag = parseETag(resETagHeader);
141+
if (resETag !== undefined) {
142+
if (reqIfNoneMatchHeader.trim() === "*") {
143+
return new Response(null, { status: 304, headers: resHeaders });
144+
}
145+
for (const reqIfNoneMatch of reqIfNoneMatchHeader.split(",")) {
146+
if (resETag === parseETag(reqIfNoneMatch)) {
147+
return new Response(null, { status: 304, headers: resHeaders });
148+
}
149+
}
150+
}
151+
}
152+
153+
// If `If-Modified-Since` is set, perform a conditional request:
154+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since
127155
const reqIfModifiedSinceHeader = reqHeaders.get("If-Modified-Since");
128156
const resLastModifiedHeader = resHeaders.get("Last-Modified");
129157
if (reqIfModifiedSinceHeader !== null && resLastModifiedHeader !== null) {
130158
const reqIfModifiedSince = parseUTCDate(reqIfModifiedSinceHeader);
131159
const resLastModified = parseUTCDate(resLastModifiedHeader);
132160
// Comparison of NaN's (invalid dates), will always result in `false`
133161
if (resLastModified <= reqIfModifiedSince) {
134-
return new Response(null, {
135-
status: 304, // Not Modified
136-
headers: resHeaders,
137-
});
162+
return new Response(null, { status: 304, headers: resHeaders });
138163
}
139164
}
140165

141-
// If `Range` is set, return a partial response
166+
// If `Range` is set, return a partial response:
167+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range
142168
const reqRangeHeader = reqHeaders.get("Range");
143169
if (reqRangeHeader !== null) {
144170
return _getRangeResponse(reqRangeHeader, resStatus, resHeaders, resBody);

packages/cache/test/cache.spec.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,37 @@ test("Cache: match throws if attempting to load cached response created with Min
238238
"load cached data created with Miniflare 1 and must delete it.",
239239
});
240240
});
241+
test("Cache: match respects If-None-Match header", async (t) => {
242+
const { cache } = t.context;
243+
const res = new Response("value", {
244+
headers: {
245+
ETag: '"thing"',
246+
"Cache-Control": "max-age=3600",
247+
},
248+
});
249+
await cache.put("http://localhost:8787/test", res);
250+
251+
const ifNoneMatch = (value: string) =>
252+
new BaseRequest("http://localhost:8787/test", {
253+
headers: { "If-None-Match": value },
254+
});
255+
256+
// Check returns 304 only if an ETag in `If-Modified-Since` matches
257+
let cacheRes = await cache.match(ifNoneMatch('"thing"'));
258+
t.is(cacheRes?.status, 304);
259+
cacheRes = await cache.match(ifNoneMatch(' W/"thing" '));
260+
t.is(cacheRes?.status, 304);
261+
cacheRes = await cache.match(ifNoneMatch('"not the thing"'));
262+
t.is(cacheRes?.status, 200);
263+
cacheRes = await cache.match(
264+
ifNoneMatch('"not the thing", "thing" , W/"still not the thing"')
265+
);
266+
t.is(cacheRes?.status, 304);
267+
cacheRes = await cache.match(ifNoneMatch("*"));
268+
t.is(cacheRes?.status, 304);
269+
cacheRes = await cache.match(ifNoneMatch(" * "));
270+
t.is(cacheRes?.status, 304);
271+
});
241272
test("Cache: match respects If-Modified-Since header", async (t) => {
242273
const { cache } = t.context;
243274
const res = new Response("value", {

0 commit comments

Comments
 (0)