A Cache API nem HTTP-cache: így marad nálad örökre a tegnapi válasz

A Service Worker Cache API-ja nem tartja be helyetted a Cache-Control szabályait. A no-store, a Vary, a lejárat és a cache-verziózás gyakorlati csapdái.

Van az a hibajegy, amelyben a szerver teljesen helyesen küld Cache-Control: no-store fejlécet, a felhasználó mégis a tegnapi, vagy rosszabb esetben az előző bejelentkezett felhasználó adatait látja. Ilyenkor könnyű a CDN-re mutogatni. Néha megérdemli, de gyakran nem ő a tettes: a Service Workerünk saját kezűleg eltette a választ a Cache API-ba.

A böngésző HTTP-cache-e és az alkalmazás Cache API-tára közti különbséget ábrázoló technikai diagram

A név megtévesztő. A Cache API nem a böngésző HTTP-cache-ének programozható távirányítója, hanem az originhez tartozó, névvel ellátott Request/Response tárolók halmaza. A böngésző csak akkor ad innen választ, ha te – tipikusan egy fetch eseménykezelőben – visszaadod neki. Ez inkább egy kicsi, lemezen tárolt adatbázis HTTP-objektumokhoz, nem pedig egy okos proxy.

Két cache, két felelős

A HTTP-cache a Cache-Control, az ETag és a többi HTTP-szabály alapján dönt. Frissnek számít-e a válasz, újra kell-e érvényesíteni, eltárolható-e egyáltalán? Ezt a böngésző intézi a hálózati kérés körül.

A Cache API-nál a döntés a te kódodé. Egy bejegyzés nem frissül magától, és nem jár le magától: addig marad, amíg delete()-tel, cache-verzióváltással vagy teljes törléssel ki nem veszed. A böngésző kvótanyomás alatt eldobhatja az origin teljes tárolóját, tehát ez nem archiválási rendszer; de a lejárat hiánya így is pontosan elég egy hónapokig élő hibához.

A kevésbé ismert részlet: a Cache.put() specifikációja a nem GET kérés, a 206-os részválasz és a Vary: * esetét utasítja el. A Cache-Control: no-store nincs ezen a listán. Ha a saját kódod meghívja a put()-ot, a szabványos HTTP-cache tiltása nem ment meg. Na, ezt nem tudtam, amíg egyszer nem láttam egy „ne cache-eld” API-választ hetekkel később is offline módban visszajönni.

A cache-first minta nem API-cache stratégia

Az alábbi, szinte minden PWA-példában felbukkanó gondolat csak immutable asseteknél biztonságos: „ha van találat, add vissza, különben töltsd le és tedd el”. Egy /assets/app.8c31.js fájlnál ez remek. Egy /api/me, keresési találat vagy kosárválasz esetében csendes adatfrissességi és könnyen adatvédelmi hiba.

Én a gyorsítótárba írás elé külön, szándékosan szűk politikát tennék. A példa ugyanazt a függvényt futtatja Service Workerben és tesztben; a no-store és a private itt nem böngészőparancs, hanem a saját tárolónk számára értelmezett szerződés.

function shouldStore(request, response, appOrigin) {
  const cacheControl = response.headers.get('Cache-Control') ?? '';

  return request.method === 'GET'
    && new URL(request.url).origin === appOrigin
    && response.ok
    && !/\b(?:no-store|private)\b/i.test(cacheControl);
}

self.addEventListener('fetch', (event) => {
  const task = (async () => {
    const cache = await caches.open('assets-v4');
    const hit = await cache.match(event.request);
    if (hit) return { response: hit };

    const response = await fetch(event.request);
    const copy = shouldStore(event.request, response, self.location.origin)
      ? response.clone()
      : null;
    return { response, cache, copy };
  })();

  event.respondWith(task.then(({ response }) => response));
  event.waitUntil(task.then(({ cache, copy }) =>
    copy ? cache.put(event.request, copy) : undefined));
});

A clone() nem dísz. A put() elfogyasztja a response törzsét, ezért ugyanazt a példányt nem adhatjuk vissza a lapnak. A waitUntil()-t ráadásul még az esemény callbackjében hívjuk meg, a cache-írás ígérete pedig később teljesül. Így az írás a Service Worker élettartamához kötődik; enélkül egy rövid életű workernek esélye van a tárolás vége előtt eltűnni. Az MDN leírása erre a látszólag fölösleges részletre adja a jó indokot.

A Vary itt sem csak egy HTTP-dísz

A Cache API találata alapból figyelembe veszi a tárolt válasz Vary fejlécét, tehát nem pusztán az URL-t nézi. A match() dokumentációja szerint ezt az ignoreVary: true opcióval ki is lehet kapcsolni. Productionben ez szinte mindig rossz ötlet: egy nyelv, tartalomtípus vagy autentikáció szerint eltérő válasznál te magad kapcsolod ki az egyetlen beépített elválasztót. A Vary segít, de nem helyettesíti azt a döntést, hogy személyes API-választ eleve nem teszünk ide.

Verziózz, takaríts, mérj

A jól használható recept kevésbé látványos, mint egy mindenre ráengedett offline réteg: külön cache az immutable build asseteknek; explicit cache-név-verzió; az activate eseményben a régi verzió törlése; dinamikus adatoknál inkább network-first vagy rövid, saját időbélyeges TTL. A Cache API nem olvassa ki neked a max-age-et, tehát ha TTL kell, azt is neked kell tárolnod és ellenőrizned.

Érdemes időnként megnézni a navigator.storage.estimate() eredményét is. Nem precíz számlakivonat, de jelzi, hogy az origin tárolója mennyire hízott el. A böngésző kvóta miatti takarítása nem invalidációs stratégia; olyan, mintha a kukásautótól várnánk a migrációk futtatását.

Productionben használnám, de csak letölthető alkalmazásburokra, hash-elt statikus assetekre és tudatos offline funkciókra. Személyes vagy gyorsan változó API-adatnál az alapértelmezett válaszom továbbra is: ne tedd bele. A Cache API ereje pont az, hogy megkerülheted a HTTP-cache szabályait. Ez egyben az oka annak is, hogy nem szabad véletlenül megkerülni őket.

Források

Leave a Reply

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük