You stored a response with the browser's Cache API and are sure the URL is right, yet `cache.match(request)` resolves with `undefined`. What are the usual causes, and which `match` options address them?
answer
- the key is a Request, not a URL string
- query string counts
- one response header is still honoured
- method is compared too
- open the right named cache
basics
~20 sCache API lookups key on the full URL including the query string, honour the stored response's Vary header, and only match GET requests. The ignoreSearch, ignoreVary and ignoreMethod options relax each rule; a wrong cache name is the fourth cause.
solid answer
~40 sFour things cause a surprising miss. First, the query string is part of the key — `/items` and `/items?page=2` are different entries, and `{ ignoreSearch: true }` makes the lookup compare only origin and path. Second, if the stored response carries a `Vary` header, the browser re-checks those request headers, so a different `Accept` or `Accept-Language` misses; `{ ignoreVary: true }` skips that check. Third, only `GET` requests match by default, so looking up a `POST` or `HEAD` request finds nothing unless you pass `{ ignoreMethod: true }`. Fourth, you may be looking in the wrong `Cache` — `cache.match()` searches one named cache, while `caches.match()` searches all of them for the origin, optionally narrowed with `{ cacheName }`. When debugging, `cache.keys()` shows the actual stored `Request` objects, which usually reveals the mismatch immediately.
code
javascript · 16 linesasync function why() {
const cache = await caches.open('lookup-demo');
await cache.put('/api/items', new Response('[]', {
headers: { 'Content-Type': 'application/json' }
}));
console.log(await cache.match('/api/items?page=2'));
// undefined - the query string is part of the key
console.log(await cache.match('/api/items?page=2', { ignoreSearch: true }));
// Response - search ignored on both sides
console.log((await cache.keys()).map((r) => `${r.method} ${r.url}`));
// shows exactly what is stored
}
why();go deeper
Remember that the cache key is the full URL including any query string, and that cache.keys() will show you exactly what was stored when a lookup surprises you.
Explain each option — ignoreSearch, ignoreVary, ignoreMethod, cacheName — and say which mismatch it fixes, including why Vary is the one response header matching still honours.
Show the risk side: relaxing matching creates false hits on paginated or content-negotiated endpoints, so argue for fixing the key rather than loosening the comparison.
Own the key design across the app: what belongs in a URL versus a header, how versioned asset URLs interact with lookups, and how invalidation stays tractable as the number of entries grows.
## What a lookup actually compares `cache.match(request, options)` runs a matching algorithm, not a string equality test on a URL. Understanding the four inputs to that algorithm explains essentially every mysterious miss. The key stored in a `Cache` is a whole `Request`, so a lookup compares the **serialised URL** (scheme, host, port, path *and* query string, with the fragment ignored), the **method**, and — if the stored *response* says so — a set of **request headers**. ## Cause 1: the query string is part of the key This is by far the most common surprise. `/api/items` and `/api/items?page=2` are two different keys, as are `?a=1&b=2` and `?b=2&a=1`, because the comparison is on the serialised URL, not on a parsed parameter set. A cache-busting suffix such as `/app.js?v=1749` likewise makes the entry unreachable from a request for `/app.js`. ```js await cache.put('/api/items', res); await cache.match('/api/items?page=2'); // undefined await cache.match('/api/items?page=2', { ignoreSearch: true }); // hit ``` `ignoreSearch: true` strips the query from both sides before comparing. Be deliberate about it: if the query genuinely selects different content, ignoring it returns the wrong body. ## Cause 2: `Vary` is enforced during lookup The Cache API ignores freshness headers entirely, but it *does* consult one header — `Vary` — and it does so on the **stored response**. If the stored response carries `Vary: Accept-Encoding, Accept-Language`, then a candidate entry only matches when the incoming request's values for those header names equal the values on the request that was originally stored. That produces misses that look impossible, because nothing about the URL changed: - A response stored during a fetch from a service worker had no `Accept-Language`, while the page's navigation request has one. - An API returns `Vary: Accept`, and one call asked for `application/json` while another sent the default. - `Vary: *` never matches anything, by definition. `{ ignoreVary: true }` disables the check. Use it when you know the variation does not affect the bytes you stored; otherwise fix the key instead — for example by storing under a synthetic URL that encodes the variant. ## Cause 3: only GET matches A `Cache` key must be a GET request with an http or https URL — `put()` rejects anything else. Matching enforces the method too, so passing a `Request` built with `method: 'POST'` or `'HEAD'` finds nothing. `{ ignoreMethod: true }` makes the algorithm skip the method comparison, which is mainly useful for looking up a HEAD request against a stored GET entry. ## Cause 4: the wrong cache `cache.match()` searches exactly one named `Cache`. If a deploy bumped the name from `static-v3` to `static-v4`, a handler still opening `static-v3` will read from an empty or stale generation. `caches.match(request)` searches every cache of the origin in creation order and returns the first hit, and accepts `{ cacheName: 'static-v4' }` to narrow it. All the other options pass through: ```js const hit = await caches.match(request, { ignoreSearch: true, cacheName: 'static-v4' }); ``` ## Debugging technique When a lookup misbehaves, stop guessing and print the keys: ```js const cache = await caches.open('static-v4'); console.log((await cache.keys()).map((r) => `${r.method} ${r.url}`)); ``` `cache.keys()` resolves with the actual stored `Request` objects in insertion order. Seeing `GET https://example.com/app.js?v=1749` next to a lookup for `/app.js` ends the investigation instantly. `cache.matchAll(request, options)` is the companion for the opposite problem — several entries matching one request, typically when `ignoreSearch` or `ignoreVary` is in play — and returns all of them rather than the first. ## The mirror-image failure The same options cause *false hits*. `ignoreSearch: true` on a paginated API returns page 1's body for a page-2 request. `ignoreVary: true` on a content-negotiated endpoint can hand JSON to a request that asked for HTML. Treat both as narrow tools for cases where you have proved the variation is irrelevant, not as a default that makes lookups "work".
- Why does `Vary` affect Cache API lookups when `Cache-Control` is ignored completely?They answer different questions. `Cache-Control` is about freshness — when an entry stops being usable — and the Cache API delegates that entirely to your code. `Vary` is about identity: it says the stored bytes are only correct for requests with those header values. Since matching is the one thing the API does decide, `Vary` is the one header it must honour.
- What is the difference between `cache.match()` and `caches.match()`?`cache.match()` searches one `Cache` you already opened. `caches.match()` is on `CacheStorage` and searches every named cache for the origin, in creation order, returning the first hit. It takes the same options plus `cacheName` to restrict the search. The plural form is convenient in a fetch handler but hides which generation answered.
- When would you use `cache.matchAll()` instead of `cache.match()`?When one request can legitimately correspond to several entries — for example with `ignoreSearch: true` across many query variants, or with `ignoreVary: true` where several negotiated representations are stored. `matchAll` resolves with every match rather than the first, which lets you pick one, or delete a whole family of entries during invalidation.
saying these in an interview costs you the question
- Assumes cache lookups compare only the path, not the query string
- Says the Cache API ignores every response header including Vary
- Thinks ignoreSearch is a harmless default to always enable
- Expects a POST request to match a stored entry
- Confuses cache.match with caches.match across all caches