Cache im Browser leeren
Als Webentwickler haben Sie vermutlich genau wie ich schon einmal versehentlich eine fehlerhafte Version eines Front-End-Assets ausgeliefert und ihm eine Cache-Lebensdauer von 30 Jahren zugewiesen. Die schlechte Nachricht ist, dass Ihre Nutzer den Cache jetzt entweder manuell löschen müssen oder aufgeschmissen sind. Oder etwa doch nicht?
Tatsächlich gibt es eine ganze Reihe von Möglichkeiten, dieses Problem zu umgehen, ohne dass Sie Ihrem Asset eine kurze TTL geben müssen. Ein weiterer Vorteil dabei ist, dass Sie Ihre Assets schnell aktualisieren können, selbst wenn gar keine fehlerhafte Version oder kein Problem vorliegt. Bei all diesen Lösungen gehe ich davon aus, dass Sie die URL des zu löschenden Assets kennen. Außerdem muss Ihre Anwendung zumindest noch eine Anfrage für ein Objekt an Ihren Server stellen, in das wir ausführbares JavaScript einbetten können, also entweder ein Skript oder eine HTML-Seite.
## location.reload(true)
Zunächst möchte ich Ihnen eine Lösung von [Steve Souders and Stoyan Stefanov came up with in 2012](https://www.stevesouders.com/blog/2012/05/22/self-updating-scripts/) vorstellen. Diese Lösung macht sich zunutze, dass die `reload()`-Methode des Objekts `location` eine boolsche Variable namens `forcedReload` verwendet, die von MDN wie folgt beschrieben wird:
> Hierbei handelt es sich um ein boolesches Flag, das beim Wert „true“ dafür sorgt, dass die Seite immer direkt vom Server neu geladen wird.
Aber werden dabei wirklich *alle* Ressourcen – ob im Cache oder nicht – direkt vom Server geladen, oder nur das Top-Level-Dokument?
Da Sie den Nutzer nicht stören wollen, indem Sie das Top-Level-Dokument offensichtlich neu laden, empfiehlt sich dafür ein iframe. Fügen Sie dazu in einem Skript im Top-Level-Dokument Folgendes ein:
```
const ifr = document.createElement(‘iframe’);
ifr.src = “/forcereload?path=/thing/stuck/in/cache”;
ifr.classList.add(“hidden-iframe”);
document.body.appendChild(ifr);
```
Folgenden Code benötigen Sie in der `/forcereload`-Antwort:
```
```
Damit das funktioniert, müssen Sie einen iframe erstellen, ein HTML-Dokument laden, das nichts mit dem Objekt zu tun hat, das wir invalidieren wollen, und es anschließend erneut laden, wobei das Objekt, das wir invalidieren wollen, ebenfalls zweimal geladen werden muss ( obwohl das erste Dokument aus dem Cache stammt). Das ist ziemlich ungünstig. Außerdem bleibt ein an das Dokument angehängter iframe erhalten, den Sie irgendwie bereinigen müssen – vermutlich mithilfe des Befehls „postmessage“ vom Frame an den Parent, um ihm mitzuteilen, dass er den Frame jetzt entfernen kann. Wie Philip Tellis richtig bemerkt, würde eine alte, aber nicht automatisch aktualisierte Version von Firefox ansonsten [go into an infinite reload loop](http://www.lognormal.com/blog/2012/06/05/updating-cached-boomerang/).
Wie sich herausstellt, [behave the way you might think it does](https://maddening-museum.glitch.me/?grep=location.reload). Das von MDN dokumentierte Argument forcedReload ist technisch gesehen nicht Teil der [the spec for the location interface](https://www.w3.org/TR/html5/browsers.html#location), und nichts ändert sich, wenn ein Browser basierend auf dem Wert dieses Arguments einen Network Fetch durchführt (zumindest gilt das für die Subresourcen). Allerdings zeigen die Browser ein unterschiedliches Verhalten bei reload() selbst. Chrome lädt die Subresource immer aus dem Cache. Firefox, Edge und Safari laden sie immer aus dem Netzwerk.
Der einzige Effekt des Arguments `forcedReload` scheint Folgender zu sein:
1. In Bezug auf *das Dokument* selbst (in unserem Fall den iframe namens „reloader“) veranlasst `forcedReload`, dass dieses in Firefox über das Netzwerk abgerufen wird, wenn es sonst aus dem Cache geholt werden würde. Alle anderen Browser laden das Dokument grundsätzlich aus dem Netzwerk.
2. In Bezug auf *Subressourcen* (wie das Skript, das wir zu aktualisieren versuchen), verhindert die Einstellung `forcedReload`, dass bedingte Anfragen gestellt werden, wenn ein beliebiger Browser (außer Chrome) eine Netzwerkanfrage für die Aktualisierung einer der Ressourcen stellt und die Header `ETag` oder `Last-Modified` vorhanden sind. In Chrome hat `forcedReload` in diesem Fall keine Auswirkung, da so oder so kein Network Fetch durchgeführt wird.
Ein weiterer Nachteil dieser Methode ist, dass es realistischerweise keine Möglichkeit gibt, zu verhindern, dass unerwünschte Einträge zum Browserverlauf hinzugefügt werden.
Dies ist die Lösung, die Steve Souders verwendet. Der Testfall, den er im Jahr 2012 zu diesem Zweck erstellt hat, funktioniert aber inzwischen in Chrome nicht mehr. Dies bestätigt auch die Ergebnisse meiner Tests. Die Ursache scheint eine Änderung im Verhalten von Chrome zu sein. Da dieses Argument nicht in der Spezifikation enthalten ist, handelt es sich technisch gesehen zwar nicht um einen Fehler, aber ich kann mir vorstellen, dass es Leute gibt, die diese Methode anwenden, und finde es schade, dass sie nicht mehr funktioniert.
## Vary + fetch
Lassen Sie uns mit einer potenziell Option weitermachen, die möglicherweise Vorteile gegenüber der ersten Variante bietet. Ich bin ein großer Fan des Vary-Headers und denke, wir können ihn hier verwenden. Er wird von sämtlichen Browsern implementiert und als Validator und nicht als Cache-Schlüssel verwendet. Wenn sich also der Wert eines Vary-Headers ändert, ist das vorhandene Objekt im Cache für die neue Anfrage ungültig und jedes *neue* Objekt, das heruntergeladen wird, ersetzt **das Objekt, das sich bereits im Cache befindet** (dieses Verhalten unterscheidet sich von CDNs und anderen "gemeinsamen" Caches, die mehrere Varianten derselben URL speichern).
Setzen wir also einen `Vary`-Header auf alle Antworten des Servers, der um etwas variiert, das nicht existiert:
```
Vary: Forced-Revalidate
```
Dies hat keinerlei Auswirkungen, da die Browser keinen `Forced-Revalidate`-Header senden, `fetch` allerdings schon:
```
await fetch(“/thing/stuck/in/cache”, {
headers: { “Forced-Revalidate”: 1 },
credentials: “include”
});
```
Aber was passiert hier eigentlich genau?
1. Sie stellen eine Anfrage für `/thing/stuck/in/cache` und es wird ein Cache-Hit gefunden. Das gecachte Objekt variiert aber um `Forced-Revalidate` mit dem Schlüssel `“”` (leerer String). Die neue Anfrage hat den `Forced-Revalidate`-Wert 1 und liefert somit keinen Match. Sie fügen der Anfrage auch Anmeldedaten hinzu, um sicherzustellen, dass die Antwort für eine normale Navigationsanfrage verwendet werden kann.
2. Die Anfrage wird an das Netzwerk gesendet. Der Server gibt die neue Version der Datei zurück, wobei die Antwort nach wie vor `Vary: Forced-Revalidate` enthält.
3. Der Browser überschreibt den vorhandenen Cache-Eintrag mit dem neuen Eintrag, der jetzt nur noch für Anfragen mit einem `Forced-Revalidate: 1`-Header gültig ist.
Aber Moment einmal! Erhalten wir für das Element im Cache jetzt nicht nur noch einen Match mit zukünftigen Anfragen, die einen `Forced-Revalidate`-Header haben? Wenn der Browser das nächste Mal aus einem gewöhnlichen Grund diese Datei lädt, zum Beispiel zur Navigation oder als Subresource, wird der spezielle Header nicht gesendet und die Datei wird *wieder* nicht aus dem Cache geladen. Diesmal hat die heruntergeladene Antwort allerdings den Vary-Schlüssel `“”` (leerer String) und ist somit für unsere Zwecke wieder nützlich.
[This is better](https://maddening-museum.glitch.me/?grep=Vary), denn jetzt verhalten sich Edge, Chrome, Firefox und Safari bei Ressourcen desselben Ursprungs korrekt. Firefox teilt den Cache für Abrufe und Navigation über mehrere Origin-Server auf, sodass der Navigations-Cache nicht gelöscht wird. Außerdem ist es möglich, dass Browser künftig mehrere Varianten speichern, wodurch diese Technik unwirksam wird. Mit einer Zeile JavaScript und ein paar komischen HTTP-Metadaten müssen Sie das Element zwar zweimal laden, aber es gibt keinen iframe und der Code lässt sich leicht pflegen.
Ideal wäre natürlich, wenn Sie anstelle der Header `{ “Forced-Revalidate”: 1 }` eingeben könnten, um Fetch eine direkte Anweisung zu geben, den Cache zu umgehen.
## fetch + cache:reload
Kommen wir also zur [cache property](https://developer.mozilla.org/en-US/docs/Web/API/Request/cache) des [Request object](https://developer.mozilla.org/en-US/docs/Web/API/Request/Request) der Fetch-API. Damit lässt sich das Problem wohl am einfachsten und besten lösen:
```
await fetch(‘/thing/stuck/in/cache’, {cache: ‘reload’, credentials: ‘include’});
```
Der Cachemodus „reload“ weist Fetch an, den Cache zu umgehen und die Anfrage direkt an das Netzwerk zu stellen, aber jede neue Antwort im Cache zu speichern. Wie zuvor fügen Sie Anmeldedaten ein, damit der Fetch für Caching-Zwecke (vermutlich) wie eine normale Navigation behandelt wird. Die neue Antwort ist sofort für alle zukünftigen Anfragen verfügbar, und Sie brauchen keine komischen Header, iframes oder ähnliches.
Klingt super, oder? Bislang funktioniert das Ganze in [works in Edge, Firefox and Safari](https://maddening-museum.glitch.me/?grep=FetchOptions). Demnächst wird diese Option auch für Chrome verfügbar sein (im Canary-Test funktioniert sie schon, allerdings läuft sie noch nicht stabil). Die Unterstützung für Ressourcen vom selben Origin-Server ist viel besser, als ich erwartet hatte. Die Support-Tabelle von MDN war veraltet, weshalb diese Option wahrscheinlich erst kürzlich in Safari und Edge eingeführt wurde.
Dennoch wird in Safari nur der Fetch-Cache geleert, und obwohl Navigationen den Fetch-Cache auffüllen können, gilt das nicht auch umgekehrt. Außerdem ist Edge der einzige Browser, der dies domänenübergreifend unterstützt.
## fetch + POST
Höchste Zeit also, ein paar größere Geschütze aufzufahren. POST-Anfragen [invalidate cached content for that URL](http://httpwg.org/specs/rfc7234.html#rfc.section.4):
> Ein Cache MUSS den tatsächlichen Request-URI (Abschnitt 5.5 von [RFC7230]) sowie den/die URI(s) in den Antwort-Header-Feldern Location und Content-Location (falls vorhanden) invalidieren, wenn als Antwort auf eine unsichere Anfragemethode kein Fehlerstatuscode zurückgegeben wird.
Die Frage ist, ob die Browser dies berücksichtigen und die Antwort cachen. [Let’s see](https://maddening-museum.glitch.me/?grep=POST+request), indem wir mit Fetch eine programmatische POST-Anfrage für die festsitzende URL generieren:
```
await fetch(‘/thing/stuck/in/cache’, {method:’POST’, credentials:’include’});
```
Sie müssen sich wohl oder übel mit einer Preflight-Anfrage zufrieden geben, da es sich hier um keine sichere Methode handelt und Sie die Anmeldedaten mit einbeziehen. Außerdem hat sich herausgestellt, dass kein Browser das Ergebnis der POST-Anfrage cacht, auch wenn dieses als cachbar angepriesen wird. Und wenn das Ergebnis doch gecacht wird, dann wird es nicht für eine nachfolgende GET-Anfrage verwendet. Selbst wenn eine Invalidierung erfolgreich durchgeführt wurde, werden also mindestens 3 Anfragen benötigt, um den Cache wieder zu füllen.
Unter dieser Prämisse schneiden Chrome und Edge hier gut ab. Da sie den Cache als Einheit betrachten, werden sowohl Inhalte invalidiert, die vom selben bzw. von verschiedenen Origin-Servern stammen, und zwar sowohl für Fetch- als auch für Navigationsobjekte. Firefox und Safari folgen demselben Muster, das wir bereits gesehen haben, indem sie Navigationen und Fetches in getrennte Caches aufteilen. Somit löscht die POST-Anfrage hier zwar den Fetch-Cache, wenn es sich bei Ihrem festsitzenden Objekt allerdings um eine Subresource handelt, haben Sie Pech.
## POST in einem iframe
Tja, aber wer A sagt, muss auch B sagen. Packen wir also eine FORM-Anfrage in einen iframe und stellen wir dort eine POST-Anfrage. Tut mir leid, aber in der Not frisst der Teufel Fliegen …
```
const ifr = document.createElement('iframe');
ifr.name = ifr.id = 'ifr_'+Date.now();
document.body.appendChild(ifr);
const form = document.createElement('form');
form.method = "POST";
form.target = ifr.name;
form.action = ‘/thing/stuck/in/cache’;
document.body.appendChild(form);
form.submit();
```
Es gibt ein paar offensichtliche Nebenwirkungen: Es wird ein Eintrag in der Browser-Historie erstellt und es besteht das gleiche Problem, dass die Antwort nicht gecacht wird. Allerdings können wir so die Preflight-Anforderungen für den Fetch umgehen, und da es sich hier um eine Navigation handelt, leeren Browser mit einem geteilten Cache so den richtigen Cache.
Diese Option [almost nails it](https://maddening-museum.glitch.me/?grep=POST+into+an+IFRAME). Firefox behält das festsitzende Objekt für Ressourcen bei, die von verschiedenen Origin-Servern stammen, allerdings nur für nachfolgende Fetches. Jeder Browser leert den Navigations-Cache für das Objekt, sowohl für Ressourcen, die vom selben Origin-Server stammen, als auch für solche, die von verschiedenen Origin-Servern stammen.
## Clear-Site-Data
Wir haben mit der unschönen Methode angefangen und uns zur perfekten Methode vorgearbeitet, nur um dann festzustellen, dass die perfekte Methode auch nicht gerade das Gelbe vom Ei ist. Damit Sie Ihre Invalidierungsziele aber doch noch erreichen können, möchte ich Ihnen abschließend eine Option vorstellen, die den Untertitel „Aus der Umlaufbahn werfen“ tragen könnte.
Lernen Sie die neue Massenvernichtungswaffe für Webentwickler kennen: [Clear-Site-Data](https://www.w3.org/TR/clear-site-data/).
Ganz gleich, welche URL Sie bereinigen möchten, Sie können diesen Response-Header ganz einfach als Antwort auf JEDE Anfrage an einen Ziel-Origin-Server zurückgeben:
```
Clear-Site-Data: “cache”
```
Schwupps, ist Ihr Cache gelöscht, und zwar nicht nur das, was Sie eigentlich löschen wollten. Der **gesamte** Cache für Ihren Origin-Server ist leer, was Ihnen im Notfall den Hintern retten könnte.
Ein weiterer Vorteil dieser Methode ist, dass Sie dabei kein clientseitiges JavaScript ausführen müssen, sodass Sie diese Antwort sogar auf eine Bild- oder Stylesheet-Anfrage senden können. Diese Methode ist also herrlich unkompliziert und gleichzeitig äußerst effektiv.
Diese Funktion ist bereits seit mehreren Jahren im Gespräch, taucht aber erst jetzt in Chrome auf, obwohl sie zum Zeitpunkt der Erstellung dieses Blogposts [reasons](https://twitter.com/mikewest/status/933277769559142400) vorübergehend deaktiviert wurde und momentan [doesn’t work in any browser](https://maddening-museum.glitch.me/?grep=Clear-Site-Data).
## Fazit
Im Folgenden noch einmal eine zusammenfassende Übersicht über die Situationen, in denen Browser tatsächlich Anfragen an das Netzwerk stellen, die den von Subressourcen verwendeten Cache invalidieren.
[1] Greift auf das Netzwerk zu, es sei denn, die Ressource hat den Header „Cache-Control: immutable“
[2] Teilt Fetch-/Navigations-Caches für fremde Origin-Server auf, sodass der Navigations-Cache nicht gelöscht wird
[3] Der Fetch invalidiert sowohl den Navigations- als auch den Fetch-Cache, aber ein anschließender Fetch füllt den Navigations-Cache nicht wieder auf.
[4] Nur der Navigations-Cache wird geleert, nicht aber der Fetch-Cache
[5] Wird bereits in Canary-Test-Versionen von Chrome unterstützt
[6] Leert nicht den Fetch-Cache
Es gibt auch noch andere Caches und Speichermöglichkeiten im Browser, auf die ich hier nicht eingehe, zum Beispiel die Service Worker Cache API. In diesem Blogpost habe ich mich auf den Umgang mit dem Cache konzentriert, den Sie mit dem HTTP-Header `Cache-Control` ansteuern können. Das Löschen anderer Speichertypen verdient einen eigenen Blogpost! Wenn Sie also ein Skript oder eine andere Subressource invalidieren möchten, **verwenden Sie die „iframe + POST“-Methode**, die in sämtlichen Browsern sowohl für Inhalte vom selben als auch von verschiedenen Origin-Servern funktioniert. Die "richtige" Methode ist aber eigentlich `cache:reload`. Hoffentlich ändern Safari und Firefox ihr Verhalten in Zukunft, damit diese Methode in der Praxis nützlicher wird.
Methode | Firefox | Safari | Edge | Chrome | |
location.reload | doc, forceReload, same-origin | Ja | Ja | Ja | Ja |
doc, normal, same-origin | Nein | Ja | Ja | Ja | |
doc, forceReload, cross-origin | Ja | Ja | Ja | Ja | |
doc, normal, cross-origin | Nein | Ja | Ja | Ja | |
resource, forceReload, same-origin | Ja | Ja | Ja | Nein | |
resource, normal, same-origin | Manchmal [1] | Ja | Ja | Nein | |
resource, forceReload, cross-origin | Ja | Ja | Ja | Nein | |
resource, normal, cross-origin | Manchmal [1] | Ja | Ja | Nein | |
Vary + fetch | same-origin | Ja | Ja [3] | Ja | Ja |
cross-origin | Nein [2] | Ja [3] | Ja | Ja | |
cache:reload | same-origin | Ja | Nein [4] | Ja | Ja [5] |
cross-origin | Nein [2] | Nein [4] | Ja | Ja [5] | |
Fetch + POST | same-origin | Ja | Nein [4] | Ja | Ja |
cross-origin | Nein [2] | Nein [4] | Ja | Ja | |
Iframe + POST | same-origin | Ja | Ja | Ja | Ja |
cross-origin | Ja [6] | Ja | Ja | Ja | |
Clear-Site-Data | Nein | Nein | Nein | Nein |
[2] Teilt Fetch-/Navigations-Caches für fremde Origin-Server auf, sodass der Navigations-Cache nicht gelöscht wird
[3] Der Fetch invalidiert sowohl den Navigations- als auch den Fetch-Cache, aber ein anschließender Fetch füllt den Navigations-Cache nicht wieder auf.
[4] Nur der Navigations-Cache wird geleert, nicht aber der Fetch-Cache
[5] Wird bereits in Canary-Test-Versionen von Chrome unterstützt
[6] Leert nicht den Fetch-Cache
Es gibt auch noch andere Caches und Speichermöglichkeiten im Browser, auf die ich hier nicht eingehe, zum Beispiel die Service Worker Cache API. In diesem Blogpost habe ich mich auf den Umgang mit dem Cache konzentriert, den Sie mit dem HTTP-Header `Cache-Control` ansteuern können. Das Löschen anderer Speichertypen verdient einen eigenen Blogpost! Wenn Sie also ein Skript oder eine andere Subressource invalidieren möchten, **verwenden Sie die „iframe + POST“-Methode**, die in sämtlichen Browsern sowohl für Inhalte vom selben als auch von verschiedenen Origin-Servern funktioniert. Die "richtige" Methode ist aber eigentlich `cache:reload`. Hoffentlich ändern Safari und Firefox ihr Verhalten in Zukunft, damit diese Methode in der Praxis nützlicher wird.