<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: etags</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/etags.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2024-03-17T19:25:34+00:00</updated><author><name>Simon Willison</name></author><entry><title>Add ETag header for static responses</title><link href="https://simonwillison.net/2024/Mar/17/add-etag-header-for-static-responses/#atom-tag" rel="alternate"/><published>2024-03-17T19:25:34+00:00</published><updated>2024-03-17T19:25:34+00:00</updated><id>https://simonwillison.net/2024/Mar/17/add-etag-header-for-static-responses/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette/pull/2306"&gt;Add ETag header for static responses&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I’ve been procrastinating on adding better caching headers for static assets (JavaScript and CSS) served by Datasette for several years, because I’ve been wanting to implement the perfect solution that sets far-future cache headers on every asset and ensures the URLs change when they are updated.&lt;/p&gt;

&lt;p&gt;Agustin Bacigalup just submitted the best kind of pull request: he observed that adding ETag support for static assets would side-step the complexity while adding much of the benefit, and implemented it along with tests.&lt;/p&gt;

&lt;p&gt;It’s a substantial performance improvement for any Datasette instance with a number of JavaScript plugins... like the ones we are building on Datasette Cloud. I’m just annoyed we didn’t ship something like this sooner!


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/etags"&gt;etags&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/web-performance"&gt;web-performance&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-cloud"&gt;datasette-cloud&lt;/a&gt;&lt;/p&gt;



</summary><category term="etags"/><category term="web-performance"/><category term="datasette"/><category term="datasette-cloud"/></entry><entry><title>ETags And Modification Times In Django</title><link href="https://simonwillison.net/2008/Dec/13/etags/#atom-tag" rel="alternate"/><published>2008-12-13T09:49:42+00:00</published><updated>2008-12-13T09:49:42+00:00</updated><id>https://simonwillison.net/2008/Dec/13/etags/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.pointy-stick.com/blog/2008/12/13/etags-and-modification-times-django/"&gt;ETags And Modification Times In Django&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Part of Malcolm’s series of tutorials on implementing advanced HTTP concepts in Django.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/etags"&gt;etags&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/malcolm-tredinnick"&gt;malcolm-tredinnick&lt;/a&gt;&lt;/p&gt;



</summary><category term="caching"/><category term="django"/><category term="etags"/><category term="http"/><category term="malcolm-tredinnick"/></entry><entry><title>ETags, ETags, ETags</title><link href="https://simonwillison.net/2007/Aug/7/mnotus/#atom-tag" rel="alternate"/><published>2007-08-07T14:51:31+00:00</published><updated>2007-08-07T14:51:31+00:00</updated><id>https://simonwillison.net/2007/Aug/7/mnotus/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.mnot.net/blog/2007/08/07/etags"&gt;ETags, ETags, ETags&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
They’re no magic bullet.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/etags"&gt;etags&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mark-nottingham"&gt;mark-nottingham&lt;/a&gt;&lt;/p&gt;



</summary><category term="etags"/><category term="http"/><category term="mark-nottingham"/></entry><entry><title>Clever Caching</title><link href="https://simonwillison.net/2007/Jul/5/caching/#atom-tag" rel="alternate"/><published>2007-07-05T00:56:33+00:00</published><updated>2007-07-05T00:56:33+00:00</updated><id>https://simonwillison.net/2007/Jul/5/caching/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.koziarski.net/archives/2007/5/28/clever-caching"&gt;Clever Caching&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Instead of invalidating your cache directly, bump a version number on your model (blog entry or whatever) and use that as part of the cache key. This also gives you dynamic etags for free.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/etags"&gt;etags&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/memcache"&gt;memcache&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/michael-koziarski"&gt;michael-koziarski&lt;/a&gt;&lt;/p&gt;



</summary><category term="caching"/><category term="etags"/><category term="memcache"/><category term="michael-koziarski"/></entry><entry><title>Supporting Conditional GET in PHP</title><link href="https://simonwillison.net/2003/Apr/23/conditionalGet/#atom-tag" rel="alternate"/><published>2003-04-23T17:22:50+00:00</published><updated>2003-04-23T17:22:50+00:00</updated><id>https://simonwillison.net/2003/Apr/23/conditionalGet/#atom-tag</id><summary type="html">
    &lt;p&gt;This site's &lt;acronym title="Really Simple Syndication"&gt;RSS&lt;/acronym&gt; feeds now support &lt;a href="https://fishbowl.pastiche.org/2002/10/21/http_conditional_get_for_rss_hackers" title="HTTP Conditional Get for RSS Hackers"&gt;Conditional GET&lt;/a&gt;. Since the feeds are dynamically generated on every request, adding support took a bit of hacking around with &lt;acronym title="PHP: Hypertext Preprocessor"&gt;PHP&lt;/acronym&gt;. Here's the function I came up with (based on the excellent description provided by Charles Miller in the article linked above):&lt;/p&gt;

&lt;div class="highlight highlight-text-html-php"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;function&lt;/span&gt; &lt;span class="pl-en"&gt;doConditionalGet&lt;/span&gt;(&lt;span class="pl-s1"&gt;&lt;span class="pl-c1"&gt;$&lt;/span&gt;timestamp&lt;/span&gt;) {
    &lt;span class="pl-c"&gt;// A PHP implementation of conditional get, see &lt;/span&gt;
    &lt;span class="pl-c"&gt;//   https://fishbowl.pastiche.org/2002/10/21/http_conditional_get_for_rss_hackers&lt;/span&gt;
    &lt;span class="pl-s1"&gt;&lt;span class="pl-c1"&gt;$&lt;/span&gt;last_modified&lt;/span&gt; = substr(date(&lt;span class="pl-s"&gt;'r'&lt;/span&gt;, &lt;span class="pl-s1"&gt;&lt;span class="pl-c1"&gt;$&lt;/span&gt;timestamp&lt;/span&gt;), &lt;span class="pl-c1"&gt;0&lt;/span&gt;, -&lt;span class="pl-c1"&gt;5&lt;/span&gt;).&lt;span class="pl-s"&gt;'GMT'&lt;/span&gt;;
    &lt;span class="pl-s1"&gt;&lt;span class="pl-c1"&gt;$&lt;/span&gt;etag&lt;/span&gt; = &lt;span class="pl-s"&gt;'"'&lt;/span&gt;.md5(&lt;span class="pl-s1"&gt;&lt;span class="pl-c1"&gt;$&lt;/span&gt;last_modified&lt;/span&gt;).&lt;span class="pl-s"&gt;'"'&lt;/span&gt;;
    &lt;span class="pl-c"&gt;// Send the headers&lt;/span&gt;
    header("&lt;span class="pl-s"&gt;Last-Modified: &lt;/span&gt;&lt;span class="pl-s1"&gt;&lt;span class="pl-c1"&gt;$&lt;/span&gt;last_modified&lt;/span&gt;");
    header("&lt;span class="pl-s"&gt;ETag: &lt;/span&gt;&lt;span class="pl-s1"&gt;&lt;span class="pl-c1"&gt;$&lt;/span&gt;etag&lt;/span&gt;");
    &lt;span class="pl-c"&gt;// See if the client has provided the required headers&lt;/span&gt;
    &lt;span class="pl-s1"&gt;&lt;span class="pl-c1"&gt;$&lt;/span&gt;if_modified_since&lt;/span&gt; = isset(&lt;span class="pl-s1"&gt;&lt;span class="pl-c1"&gt;$&lt;/span&gt;&lt;span class="pl-c1"&gt;_SERVER&lt;/span&gt;&lt;/span&gt;[&lt;span class="pl-s"&gt;'HTTP_IF_MODIFIED_SINCE'&lt;/span&gt;]) ?
        stripslashes(&lt;span class="pl-s1"&gt;&lt;span class="pl-c1"&gt;$&lt;/span&gt;&lt;span class="pl-c1"&gt;_SERVER&lt;/span&gt;&lt;/span&gt;[&lt;span class="pl-s"&gt;'HTTP_IF_MODIFIED_SINCE'&lt;/span&gt;]) :
        &lt;span class="pl-c1"&gt;false&lt;/span&gt;;
    &lt;span class="pl-s1"&gt;&lt;span class="pl-c1"&gt;$&lt;/span&gt;if_none_match&lt;/span&gt; = isset(&lt;span class="pl-s1"&gt;&lt;span class="pl-c1"&gt;$&lt;/span&gt;&lt;span class="pl-c1"&gt;_SERVER&lt;/span&gt;&lt;/span&gt;[&lt;span class="pl-s"&gt;'HTTP_IF_NONE_MATCH'&lt;/span&gt;]) ?
        stripslashes(&lt;span class="pl-s1"&gt;&lt;span class="pl-c1"&gt;$&lt;/span&gt;&lt;span class="pl-c1"&gt;_SERVER&lt;/span&gt;&lt;/span&gt;[&lt;span class="pl-s"&gt;'HTTP_IF_NONE_MATCH'&lt;/span&gt;]) : 
        &lt;span class="pl-c1"&gt;false&lt;/span&gt;;
    &lt;span class="pl-k"&gt;if&lt;/span&gt; (!&lt;span class="pl-s1"&gt;&lt;span class="pl-c1"&gt;$&lt;/span&gt;if_modified_since&lt;/span&gt; &amp;amp;&amp;amp; !&lt;span class="pl-s1"&gt;&lt;span class="pl-c1"&gt;$&lt;/span&gt;if_none_match&lt;/span&gt;) {
        &lt;span class="pl-k"&gt;return&lt;/span&gt;;
    }
    &lt;span class="pl-c"&gt;// At least one of the headers is there - check them&lt;/span&gt;
    &lt;span class="pl-k"&gt;if&lt;/span&gt; (&lt;span class="pl-s1"&gt;&lt;span class="pl-c1"&gt;$&lt;/span&gt;if_none_match&lt;/span&gt; &amp;amp;&amp;amp; &lt;span class="pl-s1"&gt;&lt;span class="pl-c1"&gt;$&lt;/span&gt;if_none_match&lt;/span&gt; != &lt;span class="pl-s1"&gt;&lt;span class="pl-c1"&gt;$&lt;/span&gt;etag&lt;/span&gt;) {
        &lt;span class="pl-k"&gt;return&lt;/span&gt;; &lt;span class="pl-c"&gt;// etag is there but doesn't match&lt;/span&gt;
    }
    &lt;span class="pl-k"&gt;if&lt;/span&gt; (&lt;span class="pl-s1"&gt;&lt;span class="pl-c1"&gt;$&lt;/span&gt;if_modified_since&lt;/span&gt; &amp;amp;&amp;amp; &lt;span class="pl-s1"&gt;&lt;span class="pl-c1"&gt;$&lt;/span&gt;if_modified_since&lt;/span&gt; != &lt;span class="pl-s1"&gt;&lt;span class="pl-c1"&gt;$&lt;/span&gt;last_modified&lt;/span&gt;) {
        &lt;span class="pl-k"&gt;return&lt;/span&gt;; &lt;span class="pl-c"&gt;// if-modified-since is there but doesn't match&lt;/span&gt;
    }
    &lt;span class="pl-c"&gt;// Nothing has changed since their last request - serve a 304 and exit&lt;/span&gt;
    header(&lt;span class="pl-s"&gt;'HTTP/1.0 304 Not Modified'&lt;/span&gt;);
    exit;
}&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Usage is simple: Work out the timestamp that the page content was last modified and call &lt;code&gt;doConditionalGet($timestamp);&lt;/code&gt;. It will send the 304 header for you and exit if the client claims to have seen the content already - otherwise control will return to your main script and you can serve content as normal. Slightly inelegant, but it does the job.&lt;/p&gt;

&lt;p&gt;Unfortunately I don't have a Conditional-GET supporting &lt;acronym title="Really Simple Syndication"&gt;RSS&lt;/acronym&gt; aggregator to hand so I have no idea if it works or not (so far I've only tested it by watching the headers sent with &lt;a href="http://livehttpheaders.mozdev.org/"&gt;LiveHTTPHeaders&lt;/a&gt;). I'd be grateful if someone could confirm that this has had the desired effect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; I've changed the above code sample (and my implementation) to send the ETag header as &lt;code&gt;ETag&lt;/code&gt; rather than &lt;code&gt;etag&lt;/code&gt;.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/charles-miller"&gt;charles-miller&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/etags"&gt;etags&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/php"&gt;php&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rss"&gt;rss&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="charles-miller"/><category term="etags"/><category term="php"/><category term="rss"/></entry></feed>