HTTP Cache-Control – ein Buch mit sieben Siegeln

Nun dachte ich ja eigentlich, dass ich mich mittlerweile ganz gut auskenne mit dem Browser-Cache und der Kontrolle über selbigen. Damit lässt sich ja eine Menge Last sparen, weil einfach ein 304 Not Modified Header an den Client gesendet wird, wenn der Inhalt im Cache noch aktuell ist. Nun habe habe ich bei einem Projekt mal wieder etwas Neues gelernt…

Und zwar geht es um den Header-Eintrag Cache-Control, wie der Titel bereits vermuten lässt. Bisher habe ich diesen Header-Eintrag so gesetzt:

header("Cache-Control: must-revalidate,public");

Ziel war es, dass der Client immer kurz den Server anfragt (must-revalidate), dabei den HTTP_IF_MODIFIED_SINCE-Header mitsendet und ich so überprüfen kann, ob der Browser-Cache-Inhalt noch aktuell ist. Wenn er noch aktuell ist, sende ich einen 304 Not Modified und beende die Scriptausführung:

$modtime_cache = @strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $modtime_page <= $modtime_cache) {
  header("HTTP/1.1 304 Not Modified");
  exit;
}

Das funktionierte auch prima – dachte ich. Ich habe mit Live HTTP Headers überprüft, ob auch 304-Header gesendet werden und alles funktionierte.

Dummerweise gab es aber ein AJAX-Login-Formular. Mit dem konnte man sich einloggen, dann wird der AJAX-Request abgeschickt und bei Erfolg einfach das Loginformular gegen den String „Eingeloggt als xyz“ ausgetauscht. Wenn man nämlich eingeloggt ist, sollte nicht der alte Inhalt aus dem Cache geladen werden, denn bei dem ist ja wieder das leere Loginformular sichtbar (und nicht der String „Eingeloggt als xyz“). Ergo dachte ich mir, dass der Browser ja anfragt, ob der Cacheinhalt noch aktuell ist und ich bei dieser Prüfung einfach noch eine Prüfung einbaue, ob der User eingeloggt ist. Wenn ja, dann sende ich einfach keinen 304. Ungefähr so:

$modtime_cache = @strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
if(empty($_SESSION['user_id']) && isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $modtime_page <= $modtime_cache) {
  header("HTTP/1.1 304 Not Modified");
  exit;
}

Und jetz kommt der Reinfall:
Ich bin auf der Startseite und logge mich also ein, der String „Eingeloggt als xyz“ erscheint, alles gut. Ich klicke auf das Logo der Webseite (das mit der Startseite verlinkt ist) und schwupps schon kommt das Loginformular wieder. Und das, obwohl die Session noch läuft und ich noch eingeloggt bin. Ich wechsle kurz auf eine andere Unterseite und da erscheint der String „Eingeloggt als xyz“. Zurück zur Startseite und plötzlich ist er auch dort zu sehen.
Ich gucke mir nochmal die HTTP-Header an und sehe, dass da überhaupt nicht „revalidatet“ wird. Der lädt einfach den Inhalt aus dem Browsercache. Ich betone nochmal: TROTZ must-revalidate.

Ich kämpfe mich nun also durch tausend Forenbeiträge, die natürlich alle erreichen wollen, dass der Browsercache überhaupt nicht genutzt wird, damit alles immer schön neu geladen wird. Performancetechnischer Blödsinn, aber diese Leute betreiben meist auch keine großen Projekte. Der Browsercache an sich ist ja eine feine Sache, wenn er tut, was man ihm sagt…und ich wollte ihn weiterhin nutzen, nur im Falle eines eingeloggten Users eben nicht.

Auf meinem Weg stoße ich immer wieder auf max-age=0, must-revalidate. Das soll bewirken, dass der Cache die gespeicherte Datei für 0 Sekunden als aktuell betrachten soll (max-age) und anschließend wieder den Server fragt, ob sie noch aktuell ist. Das klingt erstmal gut, ist es doch genau das, was ich möchte, allerdings hat es bei mir nicht funktioniert.

Dann kam no-cache auf den Plan. Nun würde man denken, dass diese Anweisung vom Namen her den Browser anweist, den Cache für dieses Dokument nicht zu verwenden. Aber Pustekuchen, no-cache bedeutet: „Lade die Datei nie direkt aus dem Cache, sondern frag erstmal beim Server nach“.
Wieso man das dann no-cache nennt, ist mir ein Rätsel…
Edit: Gut, nun lese ich mir gerade meinen eigenen Blogbeitrag zum Thema Client-Caching nochmal durch und da steht bei no-cache genau dieses Verhalten. Man muss wohl erst auf die Nase fallen, bevor man sich so etwas merkt 😉

Meine neue Anweisung für Cache-Control lautet deshalb nun:

header('Cache-Control: no-cache,must-revalidate',true);

Den Unterschied zwischen no-cache und must-revalidate konnte ich leider nicht im Netz ausfindig machen, geschweige denn, weshalb must-revalidate nicht dazu führt, dass eine Validierung stattfindet. Das public habe ich jetzt einfach mal gestrichen, weil ich eh kein SSL und solche Geschichten nutze.
Nun bin ich auf eure Kommentare gespannt. Welche Erfahrungen habt ihr mit Cache-Control? Habt ihr ähnliche Absichten wie ich damit – welche Anweisung nutzt ihr dann?

PS: Habe es jetzt auch endlich geschafft, ein Plugin zu installieren, das die „Smart Quotes“ (öffnende und schließende Anführungszeichen) hier im Blog deaktiviert. Nun kann man also Codes auch direkt kopieren, was früher meist nicht funktionierte.

Jan hat 152 Beiträge geschrieben

8 Kommentare zu “HTTP Cache-Control – ein Buch mit sieben Siegeln

  1. Ralf sagt:

    Im Prinzip ist das Verhalten logisch. Durch den Ajax-Request wird nämlich nicht das Dokument im Browser verändert, sondern JS verändert das DOM. Für den Browser hat sich am Dokument erst einmal nichts verändert.
    Ab hier liegt es am Programmierer des Browsers wie mit den Dokumenten umgegangen wird. Denn oft ist es reine Vermutung ob eine Seite aus dem Cache geholt werden kann oder ob sie neu geladen werden muss. Manche Browser sind hier etwas übereifrig und greifen aus Performance-Gründen lieber auf den Cache zurück als die Seite neu zu laden. Ein „schneller“ Browser ist ja schließlich auch ein gutes Marketing-Kriterium.

    Zur Not kann man sich auch mit einer anderen Datei aushelfen. Ist der User nicht eingeloggt, wird die index.php gesendet. Ist der User eingeloggt, wird z.B. index2.php gesendet. Dann kann man sich relativ sicher sein das die Seite neu geladen wird und im Browser-Cache zwei unterschiedliche Dokumente liegen (User eingeloggt / User nicht eingeloggt).

    Die Cache-Steuerung stammt halt aus einer Zeit als JavaScript und kurzfristige Änderungen am Dokument (Ajax) noch nicht existierten. Hier treffen halt immer noch zwei Welten aufeinander.
    Die Cache-Steuerung wurde ursprünglich nicht für Browser entwickelt, sondern für Proxy-Server. Deswegen kann es passieren das du mit deiner Lösung beim User trotzdem nicht das erreichst was du erreichen möchtest. Denn Sobald ein Proxy mit älterer Software dazwischen geschaltet ist (z.B. veraltetes Firmennetzwerk), wird es nicht mehr funktionieren. Einfach mal in die Apache-Manuals schauen:
    „Note that HTTP/1.0 caches might not implement Cache-Control and might only implement Pragma: no-cache“ (Apache-Manual)
    Ich deute das so, dass sich die Cache-Steuerung immer an das „schwächste Glied“ in der Kette hält. Ist also ein HTTP/1.0-Cache irgendwo aktiv, wird deine Cache-Steuerung ignoriert.

  2. Jan sagt:

    Kurze Frage: Ist Pragma:no-cache das gleiche wie Cache-Control:no-cache – nur eben ersteres in HTTP 1.0 und letzteres 1.1?
    Dann wäre es ja kein Problem das noch hinzuzufügen.

  3. Sammie sagt:

    Also aus meiner Sicht sollten AJAX-Requests keinen Cache nutzen und deswegen findest du auch nur solche Beiträge in den Foren. Ich mache immer:

    header(‚Cache-Control: no-cache, must-revalidate, proxy-revalidate‘);
    header(‚Pragma: no-cache‘);
    header(‚Expires: Thu, 15 Aug 1984 13:30:00 GMT‘);
    header(‚Last-Modified: ‚. gmdate(‚D, d M Y H:i:s‘) . ‚ GMT‘);

    durch ein beliebiges Expires-Datum in der Vergangenheit erzwingt man immer eine neue Version, dann wird grundsätzlich nichts aus dem Cache genutzt.

    Nur was DU genau erreichen willst ist mir nicht klar. AJAX ist (in den meisten Fällen zumindest) das Anfordern eines XML-Dokuments, dessen Inhalte dann per Javascript auseinandergepflückt und dynamisch in dem DOM-Tree eingefügt werden. Das einzige, was hier den Cache ansich beansprucht ist das Laden des XML-Dokuments. Und ob das aktuell ist oder nicht, muss auf der Serverseite analysiert werden – der Cache des Users hat damit nichts zu tun. Du validierst die Session serverseitig und schickst dem User dann entweder ein Loginformular oder einen „Hallo User“-Text. Der Browser selbst weiß doch gar nicht, ob die Session noch valide ist und es wäre auch blödsinnig, dem die Entscheidung zu überlassen.

  4. Jan sagt:

    Glaube Du hast da etwas misverstanden. Es geht nicht um das Caching des AJAX-Requests sondern um den ganz normalen URL-GET-Request – also das Aufrufen einer beliebigen Seite über eine URL. Wenn der User nicht eingeloggt ist und die Seite in der Zwischenzeit nicht akutalisiert wurde (Browsercache-Inhalt ist aktuell), dann soll sie auch aus dem Cache geladen werden statt sie neu zu generieren.

  5. Sammie sagt:

    Selbst dann versteh ich dein Problem nicht.

    Ein „Cache-Control: no-cache“-Angabe speichert trotzdem im Cache, setzt aber eine bestehende Online-Verbindung voraus, um die „Document modified“-Abfrage durchzuführen. Ist dieser Header gesetzt, kann man im Offline-Modus nicht auf diese Cache-Version zugreifen. Du zwingst damit den User sozusagen online zu sein, und auf neuere Dokumente zu prüfen, wenn er die Webseite abrufen will und damit kein reines Cache-Using (offline) zu erlauben (nur deswegen heißt es etwas verwirrenderweise „no-cache“).

    Willst du speziell erreichen, dass im eingeloggten Zustand wirklich eine neue Version geladen wird und damit der Cache umgangen wird, dann setze eine „Expires“-Header in die Vergangenheit und einen zusätzlichen „Last-Modified“-Header mit dem aktuellen Datum (siehe Bsp oben). Ohne den neuen Last-Modified-Header hat sich aus Sicht deines Browser ja nichts am Dokument geändert, deshalb ruft er auch den Cache auf beim ersten Mal.

    Zu deiner Pragma-Frage, verweise ich dich mal auf Microsoft selbst: http://support.microsoft.com/kb/234067

  6. Jan sagt:

    Das Problem ist, dass ich den Expires-Header in der Vergangenheit ja dann senden würde, wenn ich feststelle, dass der User eingeloggt ist, richtig?
    Dummerweise wird aber dann ja gar kein Request geschickt, da der Browser die Datei einfach aus dem lokalen Cache lädt, ohne nachzufragen, ob die Datei noch aktuell ist.

    Falls ich es immernoch nicht verstanden habe, schicke mal bitte ein Beispiel mit datei.php – wie würdest Du die Header setzen, wenn der User nicht eingeloggt ist und wie, wenn er eingeloggt ist? Ich denke, dann wird es klarer.

  7. Ralf sagt:

    Ab hier wird es wohl richtig kompliziert.
    User nicht eingeloggt: Expires in der Gegenwart (Seite wird aus dem Cache geladen)
    User eingeloggt und Seite wurde von Ajax geändert: Expires in der Vergangenheit (Seite wird neu vom Server geholt)
    User eingeloggt und Dokument wird erneut aufgerufen: Expires in der Gegenwart (Seite wird aus dem Cache geladen)

    Tja, da hast du wahrscheinlich ein Problem. Denn so auf den ersten Blick wird eine Version der Seite immer aktuell geladen und nur eine Version kann im Cache gespeichert werden. Richtig kompliziert wird es ja dann, wenn der User sich ausloggt und die Seite nicht verlässt (sowas soll ja auch vorkommen).

Eine Antwort schreiben

Ihre E-Mail-Adresse wird nicht veröffentlicht. Benötigte Felder sind markiert mit *

You may use these HTML tags and attributes: <a href=""> <blockquote cite=""> <pre lang=""> <b> <strong> <i> <em>