Die HTTP Spezifikation definiert, wie HTTP Header Einträge Clients und Proxys mit Caching-Anweisungen versorgen. Seit HTTP1.1 stehen umfangreiche Möglichkeiten zur Steuerung des Cachings bereit. Ein unbedachter oder vernachlässigter Einsatz dieser Einträge kann folgenschwer sein:
Die HTTP Header Date, Expires, Pragma und Cache-Control spielen dabei die wichtigste Rolle. Eine explizite Angabe dieser Werte ist außerdem wichtig, da das Verhalten von Clients und Proxys bei fehlenden Angaben nicht standardisiert ist. Vernünftige Werte für diese Attribute erläutern sich am besten aus der Sicht der drei wichtigen Anwendungsfälle beim Cachen:
Die Inhalte dieser Seite ändern sich potenziell und sind nie aktuell:
Date: <aktuelle Serverzeit> Expires: Sat, 6 May 1995 12:00:00 GMT // Hauptsache in der Vergangenheit Cache-Control: no-cache Cache-Control: post-check=0,pre-check=0 // für die Freunde des IE, proprietär Pragma: no-cache // für die Freunde von HTTP1.0Aber Achtung! Verwirrenderweise bedeutet no-cache NICHT, dass ein Response niemals gecached wird. Insbesondere im Zusammenhang mit dem Conditional GET werden auch als no-cache gekennzeichnete Antworten regulär gecached. Eine zusätzliche Angabe von no-store in Cache-Control weist den Browser an, unter keinen Umständen den Inhalt dieser Seite clientseitig permanent zu speichern. Aber auch hier gilt große Vorsicht, dieser Eintrag garantiert NICHT die Geheimhaltung der Informationen. So mag ein Client dennoch die Inhalte, zum Beispiel als History-Eintrag, lokal vorhalten. Das ganze ist so herrlich verwirrend, dass ich es nochmal als Tabelle festhalte:
Cache-Control | Was man vermutet | Was die Spezifikation sagt |
---|---|---|
no-cache | Clientseitig erfolgt kein Caching: FALSCH! | der Client KANN cachen, zum Beispiel im Rahmen des Conditional GET |
must-revalidate | Die Daten im Cache werden validiert: FALSCH! | Die Validierung erfolgt sowieso, da no-cache angegeben wurde. Aber wenn die Validierung nicht durchgeführt werden kann (sprich: kein Netzwerk), dann muss ein Fehler angezeigt werden. |
no-store | Die Daten werden niemals clientseitig gespeichert: FALSCH! | Speichern im Clientcache erfolgt trotzdem, aber es wird VERSUCHT, die Inhalte andersweitig 'sicher' zu behandeln. Sie werden beispielsweise nicht dauerhaft gespeichert. |
Wie erwähnt ist das Verhalten von Clients und Proxys ohne irgendwelche Angaben zum Cachen nicht spezifiziert. In den alten Tagen des Internets mit gringer Bandbreite war es ein durchaus plausibles Verhalten, solche Inhalte für eine willkürliche Zeit vorzuhalten. So gesehen ist die no-cache Direktive eine Aufforderung an Clients, dies nicht zu tun.
Die Daten sind nutzerspezifisch und bleiben x Sekunden aktuell:
Date: <aktuelle Serverzeit> Expires: <aktuelle Serverzeit + x Sekunden> Cache-Control: privateDie Angabe in Expires kann mit der max-age Direktive überschrieben werden. Existiert die Angabe, dann wird diese berücksichtigt, selbst wenn die Angaben bei Expires restriktiver sind.
Date: <aktuelle Serverzeit> Expires: <..> // Angabe wird ignoriert, da max-age gesetzt Cache-Control: private, max-age=<x Sekunden>
3. Cachen im HTTP Client und im HTTP Proxy
Die Daten sind für alle Nutzer gleich (nicht nutzerspezifisch) und bleiben x Sekunden aktuell:
Date: <aktuelle Serverzeit> Expires: <aktuelle Serverzeit + x Sekunden> Cache-Control: publicWieder kann mit der Angabe von max-age die Angabe von Expires überschrieben werden. Die Angabe von public ist dann überflüssig, da max-age diese impliziert:
Date: <aktuelle Serverzeit> Expires: <..> // Angabe wird ignoriert, da max-age gesetzt Cache-Control: max-age=<x Sekunden> // impliziert 'public'Alle Zeitangaben sind entsprechend der HTTP Spezifikation zu formatieren (Beispiel: Fri, 30 Oct 2010 14:19:41 GMT), am besten man benutzt Servicemethoden wie setDateHeader in HttpServletResponse.
Server können jederzeit GET Anfragen mit einem Statuscode HTTP 304 Not Modified beantworten. In diesem Fall signalisiert der Server dem anfragenden Client, dass die Ressource in clientseitigen Cache aktuell ist und wiederverwendet werden kann. Offenbar sind zwei Dinge für diesen Mechanismus nötig: a) Beim Versenden einer Ressource muss der Server dem Client die Zeit der Erstellung mitgeben und b) der Server muss wissen, ob sich die Ressource inzwischen geändert hat. Technisch wird dies über den Last-Modified Eintrag im Header des Responses erreicht - beim nächsten Request wird dieser Wert als If-Modified-Since wieder zum Server geschickt und kann ausgewertet werden.
Für Servlets ist die praktische Umsetzung ganz einfach. Man überschreibt getLastModified im Servlet, der Rest - also das Setzen des Last-Modified Eintrag und die Auswertung des If-Modified-Since geschieht automatisch über die Implementierung in HttpServlet:
@Override protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException { // setzte relevante Header und erlaube Caching, der Date Header wird automatisch gesetzt response.setContentType("text/html;charset=UTF-8"); // Seiteninhalte ab hier, die Logik für Conditional GET ist im HttpServlet, basierend // auf getLastModified implementiert : } : @Override protected long getLastModified(HttpServletRequest req) { // Testweise: simuliert neuen Content alle 10 Sekunden long lastModified = System.currentTimeMillis(); lastModified /= 10000; lastModified *= 10000; return lastModified; }Zum Test lädt man die Seite dauernd, sie wird nur alle 10 Sekunden neu erzeugt und geladen (Status 200 OK), dazwischen wird ein Status 304 gesendet und der Client nutzt die Version im Cache.
Für JSPs und darauf basierenden Technologien ist das Vorgehen nicht so einfach, da der generierte JSP Code nicht von HttpServlet erbt (getLastModified wird nie gerufen!). Die Lösung besteht darin, den Last-Modified Eintrag im Response zu setzen und den If-Modified-Since im Request auszuwerten - mit anderen Worten Conditional GET selber zu implementieren:
<% // simuliert neue Inhalte alle 10 Sekunden long lastModified = System.currentTimeMillis(); lastModified /= 10000; lastModified *= 10000; // long ifModifiedSince = request.getDateHeader("If-Modified-Since"); if (ifModifiedSince != -1 && !response.isCommitted() && ifModifiedSince >= lastModified) { // schicke 304 Not Modified - der Client soll seinen Cache nutzen response.setStatus(javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED); return; } // ab hier Seiteninhalte response.setDateHeader("Last-Modified", lastModified); : %>Es ist mir nicht klar, warum es in der JSP Spezifikation dafür keine Unterstützung gibt.
Kann nicht anhand einer Zeitangabe die Aktualität einer Ressource validiert werden, dann muss der entity tag einer Ressource zur Anwendung kommen. Dazu sendet der Server im Header einen Wert für ETag - der ist technisch gesehen ein möglichst einfach zu ermittelnder Hash der betreffenden Ressource. Diese Angabe wird vom Client als If-None-Match in der nächsten Anfrage zum Server zurückgeschickt. Beim ETag gibt es keine Methode analog getLastModified im Servlet die man überschreiben könnte. Hier ist wieder Handarbeit gefordert:
@Override protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException { // response.setContentType("text/html;charset=UTF-8"); // ermittle den aktuelle ETag String currentETag = getETag(); // lese den ETag aus dem Client-Request bei Gleichheit // wird 304 Not Modified zum Client geschickt if(currentETag != null && !response.isCommitted() && currentETag.equals(request.getHeader("If-None-Match"))) { response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); return; } // response.setHeader("ETag", currentETag); // ab hier Seitenausgabe : } : private String getETag() { // // Der ETag der Ressource ist typischerweise ein Hash der // Ressource. Hier wird beispielhaft ein ETag simuliert der sich alle 10 Sekunden ändert long currentTime = System.currentTimeMillis(); // Auf diese Weise ändert sich current Time nur alle 10 Sekunden: currentTime /= 10000; currentTime *= 10000; // // man beachte die Quotierung, ist von der Spec so gefordert return "/"" + currentTime + "/""; }Für JSP verfährt man analog Last-Modified im letzten Abschnitt. Auch hier ist nicht klar, warum es in der Servlet Spezifikation für den ETag keine Unterstützung ähnlich getLastModified gibt.
Ermittlung von ETag und Last-Modified
Die gerade gegebenen Beispiele für die Verwendung des ETag- und Last-Modified Header funktionieren, verschleiern aber etwas: in der Praxis lassen sich die Werte erst ermitteln, nachdem alle fachlichen Informationen zur Erstellung der Seite bekannt sind. Fachliche Inhalte werden demnach doppelt abgefragt, einmal für die Berechnung von ETag/Last-Modified und ein weiteres Mal, wenn nötig, für das Erstellen der View. Das kommt natürlich nur dann in Frage, wenn fachliche Inhalte billig zu ermitteln sind - was in den seltendsten Fällen so sein wird. Welche anderen Lösungen bieten sich an:
// Pseudocode: // // hole fachliche Daten ResultSet = SELECT x FROM y // generiere die Inhalte und ermittle gleichzeitig ETag/Last-Modified while (ResultSet.hasNext()) { writer.write(nextResult); etag = etag + hash(nextResult); } : // Conditional GET wenn noch nichts committed if (!response.isCommitted() && etag.equals(request.getHeader("If-None-Match"))) { response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); return; } // setze den ETag im Header, Quotes nicht vergessen response.addHeader("ETag", '"' + etag + '"'); // Inhalte werden zum Client gesendet return;Wie gesagt: das kann nur funktionieren, wenn der Response-Puffer groß genug eingestellt wurde. Außerdem muss man sich klar machen, dass die Response zwar vielleicht nicht zum Client geschickt wird, sie wird aber komplett generiert und liegt im Speicher des Servers bis sie verschickt oder verworfen werden kann.
Werden POST Anfragen gecached?
Für eine Erklärung muss man etwas ausholen: ein POST Request hat formal keinen Inhalt als Response, da ein POST Request nicht die URI einer Ressource adressiert, sondern eine Handlungsaufforderung an den Server darstellt. Erst die Antwort des Servers gibt neben dem Status des POST Requests auch einen Hinweis, wie der Client weiterarbeiten sollte. Der korrekte Ablauf sieht typischerweise so aus:
Dieser Ablauf wird als PRG-Pattern bezeichnet und wenn man es implementiert stellt sich die Frage nach dem Caching eines POST Requests nicht. In der Praxis wird jedoch der POST Request des Clients häufig mit Status HTTP 200 OK und Inhalten beantwortet und bedeutet eine Verletzung der POST-Semantik: der Request wird gleichzeitig als Handlungsaufforderung und als Adresse zu einer URI, deren Content als Response geliefert wird, gehandhabt. Eine solche Response kann laut HTTP Spezifikation, Kapitel 9 auch für POST Requests gecached werden, wenn die Header entsprechend gesetzt sind - gemeint ist damit: für zukünftige GET Requests derselben URI. Der POST Request wird aber immer zum Server geschickt - alles andere macht konzeptionell auch kein Sinn.
GET | POST | |
---|---|---|
Semantik | fordert eine Ressource mit einer URI an, HTTP safe und idempotent | Handlungsanweisung an den Server, ändert Ressource, nicht HTTP safe, nicht idempotent |
Caching | ja, wenn die Header Date, Content-Cache und Expires gesetzt sind. Die Inhalte von POST Anfragen werden zwar gecached, können aber nur bei GET Anfragen derselben URL genutzt werden | ebenso |
Conditional GET | ja, mit Last-Modified oder ETag, Browser versuchen hier POST und GET Anfragen gleich zu behandeln | ebenso |
Zusammenfassung - Best Practices für das Cachen bei Webanwendungen
Und wie immer gilt auch hier: Testen Sie mit der Hilfe geeigneter Werkzeuge die Ergebnisse Ihrer Bemühungen und unterlassen Sie Optimierungen, wenn kein Effekt festgestellt werden kann.
Hypertext Transfer Protocol -- HTTP/1.1
Caching Tutorial for Webmasters
Google Articel HttpCaching
HTTP caching
Eine Seite, die mit XMLHttpRequests das Cacheverhalten des Browsers prüft
Web Caching Article von Brian D. Davison