<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: http</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/http.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2026-05-05T23:44:27+00:00</updated><author><name>Simon Willison</name></author><entry><title>datasette-referrer-policy 0.1</title><link href="https://simonwillison.net/2026/May/5/datasette-referrer-policy/#atom-tag" rel="alternate"/><published>2026-05-05T23:44:27+00:00</published><updated>2026-05-05T23:44:27+00:00</updated><id>https://simonwillison.net/2026/May/5/datasette-referrer-policy/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/datasette/datasette-referrer-policy/releases/tag/0.1"&gt;datasette-referrer-policy 0.1&lt;/a&gt;&lt;/p&gt;
        &lt;p&gt;The OpenStreetMap tiles on the Datasette &lt;a href="https://datasette.io/global-power-plants/global-power-plants"&gt;global-power-plants demo&lt;/a&gt; weren't displaying correctly. This turned out to be caused by two bugs.&lt;/p&gt;
&lt;p&gt;The first is that the CAPTCHA &lt;a href="https://github.com/simonw/datasette-turnstile"&gt;I added&lt;/a&gt; to that site a few weeks ago was triggering for the &lt;code&gt;.json&lt;/code&gt; fetch requests used by the map plugin, and since those weren't HTML the user was not being asked to solve them. Here's &lt;a href="https://github.com/simonw/datasette.io/commit/23a1c8596b75b2094db46035a3b4280109fb3df3"&gt;the fix&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The second was that OpenStreetMap quite reasonably &lt;a href="https://wiki.openstreetmap.org/wiki/Referer"&gt;block tile requests&lt;/a&gt; from sites that use a &lt;code&gt;Referrer-Policy: no-referrer&lt;/code&gt; header.&lt;/p&gt;
&lt;p&gt;Datasette does this by default, and I didn't want to change that default on people without warning - so I had Codex + GPT-5.5 &lt;a href="https://gisthost.github.io/?402f2f23ee3dbfa251bf0d216e0224f7"&gt;build me&lt;/a&gt; a new plugin to help set that header to another value.&lt;/p&gt;
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/openstreetmap"&gt;openstreetmap&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="http"/><category term="openstreetmap"/><category term="datasette"/></entry><entry><title>Unicode Explorer using binary search over fetch() HTTP range requests</title><link href="https://simonwillison.net/2026/Feb/27/unicode-explorer/#atom-tag" rel="alternate"/><published>2026-02-27T17:50:54+00:00</published><updated>2026-02-27T17:50:54+00:00</updated><id>https://simonwillison.net/2026/Feb/27/unicode-explorer/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/unicode-binary-search"&gt;Unicode Explorer using binary search over fetch() HTTP range requests&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Here's a little prototype I built this morning from my phone as an experiment in HTTP range requests, and a general example of using LLMs to satisfy curiosity.&lt;/p&gt;
&lt;p&gt;I've been collecting &lt;a href="https://simonwillison.net/tags/http-range-requests/"&gt;HTTP range tricks&lt;/a&gt; for a while now, and I decided it would be fun to build something with them myself that used binary search against a large file to do something useful.&lt;/p&gt;
&lt;p&gt;So I &lt;a href="https://claude.ai/share/47860666-cb20-44b5-8cdb-d0ebe363384f"&gt;brainstormed with Claude&lt;/a&gt;. The challenge was coming up with a use case for binary search where the data could be naturally sorted in a way that would benefit from binary search.&lt;/p&gt;
&lt;p&gt;One of Claude's suggestions was looking up information about unicode codepoints, which means searching through many MBs of metadata.&lt;/p&gt;
&lt;p&gt;I had Claude write me a spec to feed to Claude Code - &lt;a href="https://github.com/simonw/research/pull/90#issue-4001466642"&gt;visible here&lt;/a&gt; - then kicked off an &lt;a href="https://simonwillison.net/2025/Nov/6/async-code-research/"&gt;asynchronous research project&lt;/a&gt; with Claude Code for web against my &lt;a href="https://github.com/simonw/research"&gt;simonw/research&lt;/a&gt; repo to turn that into working code.&lt;/p&gt;
&lt;p&gt;Here's the &lt;a href="https://github.com/simonw/research/tree/main/unicode-explorer-binary-search#readme"&gt;resulting report and code&lt;/a&gt;. One interesting thing I learned is that Range request tricks aren't compatible with HTTP compression because they mess with the byte offset calculations. I added &lt;code&gt;'Accept-Encoding': 'identity'&lt;/code&gt; to the &lt;code&gt;fetch()&lt;/code&gt; calls but this isn't actually necessary because Cloudflare and other CDNs automatically skip compression if a &lt;code&gt;content-range&lt;/code&gt; header is present.&lt;/p&gt;
&lt;p&gt;I deployed the result &lt;a href="https://tools.simonwillison.net/unicode-binary-search"&gt;to my tools.simonwillison.net site&lt;/a&gt;, after first tweaking it to query the data via range requests against a CORS-enabled 76.6MB file in an S3 bucket fronted by Cloudflare.&lt;/p&gt;
&lt;p&gt;The demo is fun to play with - type in a single character like &lt;code&gt;ø&lt;/code&gt; or a hexadecimal codepoint indicator like &lt;code&gt;1F99C&lt;/code&gt; and it will binary search its way through the large file and show you the steps it takes along the way:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Animated demo of a web tool called Unicode Explore. I enter the ampersand character and hit Search. A box below shows a sequence of HTTP binary search requests made, finding in 17 steps with 3,864 bytes transferred and telling me that ampersand is U+0026 in Punctuation other, Basic Latin" src="https://static.simonwillison.net/static/2026/unicode-explore.gif" /&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/algorithms"&gt;algorithms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/research"&gt;research&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tools"&gt;tools&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/unicode"&gt;unicode&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vibe-coding"&gt;vibe-coding&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http-range-requests"&gt;http-range-requests&lt;/a&gt;&lt;/p&gt;



</summary><category term="algorithms"/><category term="http"/><category term="research"/><category term="tools"/><category term="unicode"/><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="vibe-coding"/><category term="http-range-requests"/></entry><entry><title>Unicode Explorer — Binary Search Over HTTP</title><link href="https://simonwillison.net/2026/Feb/27/unicode-binary-search/#atom-tag" rel="alternate"/><published>2026-02-27T16:01:33+00:00</published><updated>2026-02-27T16:01:33+00:00</updated><id>https://simonwillison.net/2026/Feb/27/unicode-binary-search/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Tool:&lt;/strong&gt; &lt;a href="https://tools.simonwillison.net/unicode-binary-search"&gt;Unicode Explorer — Binary Search Over HTTP&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/unicode"&gt;unicode&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http-range-requests"&gt;http-range-requests&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="http"/><category term="unicode"/><category term="http-range-requests"/></entry><entry><title>Unicode Explorer — Binary Search Over HTTP</title><link href="https://simonwillison.net/2026/Feb/27/unicode-explorer-binary-search/#atom-tag" rel="alternate"/><published>2026-02-27T16:00:00+00:00</published><updated>2026-02-27T16:00:00+00:00</updated><id>https://simonwillison.net/2026/Feb/27/unicode-explorer-binary-search/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Research:&lt;/strong&gt; &lt;a href="https://github.com/simonw/research/tree/main/unicode-explorer-binary-search#readme"&gt;Unicode Explorer — Binary Search Over HTTP&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/unicode"&gt;unicode&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http-range-requests"&gt;http-range-requests&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="http"/><category term="unicode"/><category term="http-range-requests"/></entry><entry><title>Introducing gisthost.github.io</title><link href="https://simonwillison.net/2026/Jan/1/gisthost/#atom-tag" rel="alternate"/><published>2026-01-01T22:12:20+00:00</published><updated>2026-01-01T22:12:20+00:00</updated><id>https://simonwillison.net/2026/Jan/1/gisthost/#atom-tag</id><summary type="html">
    &lt;p&gt;I am a huge fan of &lt;a href="https://gistpreview.github.io/"&gt;gistpreview.github.io&lt;/a&gt;, the site by Leon Huang that lets you append &lt;code&gt;?GIST_id&lt;/code&gt; to see a browser-rendered version of an HTML page that you have saved to a Gist. The last commit was ten years ago and I needed a couple of small changes so I've forked it and deployed an updated version at &lt;a href="https://gisthost.github.io/"&gt;gisthost.github.io&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="some-background-on-gistpreview"&gt;Some background on gistpreview&lt;/h4&gt;
&lt;p&gt;The genius thing about &lt;code&gt;gistpreview.github.io&lt;/code&gt; is that it's a core piece of GitHub infrastructure, hosted and cost-covered entirely by GitHub, that wasn't built with any involvement from GitHub at all.&lt;/p&gt;
&lt;p&gt;To understand how it works we need to first talk about Gists.&lt;/p&gt;
&lt;p&gt;Any file hosted in a &lt;a href="https://gist.github.com/"&gt;GitHub Gist&lt;/a&gt; can be accessed via a direct URL that looks like this:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;https://gist.githubusercontent.com/simonw/d168778e8e62f65886000f3f314d63e3/raw/79e58f90821aeb8b538116066311e7ca30c870c9/index.html&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;That URL is served with a few key HTTP headers:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These ensure that every file is treated by browsers as plain text, so HTML file will not be rendered even by older browsers that attempt to guess the content type based on the content.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Via: 1.1 varnish
Cache-Control: max-age=300
X-Served-By: cache-sjc1000085-SJC
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These confirm that the file is sever via GitHub's caching CDN, which means I don't feel guilty about linking to them for potentially high traffic scenarios.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Access-Control-Allow-Origin: *
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is my favorite HTTP header! It means I can hit these files with a &lt;code&gt;fetch()&lt;/code&gt; call from any domain on the internet, which is fantastic for building &lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/"&gt;HTML tools&lt;/a&gt; that do useful things with content hosted in a Gist.&lt;/p&gt;
&lt;p&gt;The one big catch is that Content-Type header. It means you can't use a Gist to serve HTML files that people can view.&lt;/p&gt;
&lt;p&gt;That's where &lt;code&gt;gistpreview&lt;/code&gt; comes in. The &lt;code&gt;gistpreview.github.io&lt;/code&gt; site belongs to the dedicated &lt;a href="https://github.com/gistpreview"&gt;gistpreview&lt;/a&gt; GitHub organization, and is served out of the &lt;a href="https://github.com/gistpreview/gistpreview.github.io"&gt;github.com/gistpreview/gistpreview.github.io&lt;/a&gt; repository by GitHub Pages.&lt;/p&gt;
&lt;p&gt;It's not much code. The key functionality is this snippet of JavaScript from &lt;a href="https://github.com/gistpreview/gistpreview.github.io/blob/master/main.js"&gt;main.js&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-en"&gt;fetch&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'https://api.github.com/gists/'&lt;/span&gt; &lt;span class="pl-c1"&gt;+&lt;/span&gt; &lt;span class="pl-s1"&gt;gistId&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;then&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-k"&gt;function&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;res&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-s1"&gt;res&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;json&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;then&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-k"&gt;function&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;body&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;res&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;status&lt;/span&gt; &lt;span class="pl-c1"&gt;===&lt;/span&gt; &lt;span class="pl-c1"&gt;200&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
      &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-s1"&gt;body&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-kos"&gt;}&lt;/span&gt;
    &lt;span class="pl-smi"&gt;console&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;log&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;res&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;body&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt; &lt;span class="pl-c"&gt;// debug&lt;/span&gt;
    &lt;span class="pl-k"&gt;throw&lt;/span&gt; &lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-v"&gt;Error&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'Gist &amp;lt;strong&amp;gt;'&lt;/span&gt; &lt;span class="pl-c1"&gt;+&lt;/span&gt; &lt;span class="pl-s1"&gt;gistId&lt;/span&gt; &lt;span class="pl-c1"&gt;+&lt;/span&gt; &lt;span class="pl-s"&gt;'&amp;lt;/strong&amp;gt;, '&lt;/span&gt; &lt;span class="pl-c1"&gt;+&lt;/span&gt; &lt;span class="pl-s1"&gt;body&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;message&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;replace&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-pds"&gt;&lt;span class="pl-c1"&gt;/&lt;/span&gt;&lt;span class="pl-cce"&gt;\(&lt;/span&gt;.&lt;span class="pl-c1"&gt;*&lt;/span&gt;&lt;span class="pl-cce"&gt;\)&lt;/span&gt;&lt;span class="pl-c1"&gt;/&lt;/span&gt;&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;''&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;then&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-k"&gt;function&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;info&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;fileName&lt;/span&gt; &lt;span class="pl-c1"&gt;===&lt;/span&gt; &lt;span class="pl-s"&gt;''&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-k"&gt;var&lt;/span&gt; &lt;span class="pl-s1"&gt;file&lt;/span&gt; &lt;span class="pl-k"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;info&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;files&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
      &lt;span class="pl-c"&gt;// index.html or the first file&lt;/span&gt;
      &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;fileName&lt;/span&gt; &lt;span class="pl-c1"&gt;===&lt;/span&gt; &lt;span class="pl-s"&gt;''&lt;/span&gt; &lt;span class="pl-c1"&gt;||&lt;/span&gt; &lt;span class="pl-s1"&gt;file&lt;/span&gt; &lt;span class="pl-c1"&gt;===&lt;/span&gt; &lt;span class="pl-s"&gt;'index.html'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
        &lt;span class="pl-s1"&gt;fileName&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;file&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
      &lt;span class="pl-kos"&gt;}&lt;/span&gt;
    &lt;span class="pl-kos"&gt;}&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;
  &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;info&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;files&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;hasOwnProperty&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;fileName&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;===&lt;/span&gt; &lt;span class="pl-c1"&gt;false&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-k"&gt;throw&lt;/span&gt; &lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-v"&gt;Error&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'File &amp;lt;strong&amp;gt;'&lt;/span&gt; &lt;span class="pl-c1"&gt;+&lt;/span&gt; &lt;span class="pl-s1"&gt;fileName&lt;/span&gt; &lt;span class="pl-c1"&gt;+&lt;/span&gt; &lt;span class="pl-s"&gt;'&amp;lt;/strong&amp;gt; is not exist'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;
  &lt;span class="pl-k"&gt;var&lt;/span&gt; &lt;span class="pl-s1"&gt;content&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;info&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;files&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-s1"&gt;fileName&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;content&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-smi"&gt;document&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;write&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;content&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This chain of promises fetches the Gist content from the GitHub API, finds the section of that JSON corresponding to the requested file name and then outputs it to the page like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-smi"&gt;document&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;write&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;content&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This is smart. Injecting the content using &lt;code&gt;document.body.innerHTML = content&lt;/code&gt; would fail to execute inline scripts. Using &lt;code&gt;document.write()&lt;/code&gt; causes the browser to treat the HTML as if it was directly part of the parent page.&lt;/p&gt;
&lt;p&gt;That's pretty much the whole trick! Read the Gist ID from the query string, fetch the content via the JSON API and &lt;code&gt;document.write()&lt;/code&gt; it into the page.&lt;/p&gt;
&lt;p&gt;Here's a demo:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://gistpreview.github.io/?d168778e8e62f65886000f3f314d63e3"&gt;https://gistpreview.github.io/?d168778e8e62f65886000f3f314d63e3&lt;/a&gt;&lt;/p&gt;
&lt;h4 id="fixes-for-gisthost-github-io"&gt;Fixes for gisthost.github.io&lt;/h4&gt;
&lt;p&gt;I forked &lt;code&gt;gistpreview&lt;/code&gt; to add two new features:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A workaround for Substack mangling the URLs&lt;/li&gt;
&lt;li&gt;The ability to serve larger files that get truncated in the JSON API&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I also removed some dependencies (jQuery and Bootstrap and an old &lt;code&gt;fetch()&lt;/code&gt; polyfill) and inlined the JavaScript into &lt;a href="https://github.com/gisthost/gisthost.github.io/blob/main/index.html"&gt;a single index.html file&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The Substack issue was small but frustrating. If you email out a link to a &lt;code&gt;gistpreview&lt;/code&gt; page via Substack it modifies the URL to look like this:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://gistpreview.github.io/?f40971b693024fbe984a68b73cc283d2=&amp;amp;utm_source=substack&amp;amp;utm_medium=email"&gt;https://gistpreview.github.io/?f40971b693024fbe984a68b73cc283d2=&amp;amp;utm_source=substack&amp;amp;utm_medium=email&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This breaks &lt;code&gt;gistpreview&lt;/code&gt; because it treats &lt;code&gt;f40971b693024fbe984a68b73cc283d2=&amp;amp;utm_source...&lt;/code&gt; as the Gist ID.&lt;/p&gt;
&lt;p&gt;The fix is to read everything up to that equals sign. I &lt;a href="https://github.com/gistpreview/gistpreview.github.io/pull/7"&gt;submitted a PR&lt;/a&gt; for that back in November.&lt;/p&gt;
&lt;p&gt;The second issue around truncated files was &lt;a href="https://github.com/simonw/claude-code-transcripts/issues/26#issuecomment-3699668871"&gt;reported against my claude-code-transcripts project&lt;/a&gt; a few days ago. Here's &lt;a href="https://github.com/gisthost/gisthost.github.io/commit/d083845269dda20ae1d0071925b68558d2ea4fdf"&gt;the fix&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;That project provides a CLI tool for exporting HTML rendered versions of Claude Code sessions. It includes a &lt;code&gt;--gist&lt;/code&gt; option which uses the &lt;code&gt;gh&lt;/code&gt; CLI tool to publish the resulting HTML to a Gist and returns a gistpreview URL that the user can share.&lt;/p&gt;
&lt;p&gt;These exports can get pretty big, and some of the resulting HTML was past the size limit of what comes back from the Gist API.&lt;/p&gt;
&lt;p&gt;As of &lt;a href="https://github.com/simonw/claude-code-transcripts/releases/tag/0.5"&gt;claude-code-transcripts 0.5&lt;/a&gt; the &lt;code&gt;--gist&lt;/code&gt; option now publishes to &lt;a href="https://gisthost.github.io/"&gt;gisthost.github.io&lt;/a&gt; instead, fixing both bugs.&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://gisthost.github.io/?02ced545666128ce4206103df6185536"&gt;the Claude Code transcript&lt;/a&gt; that refactored Gist Host to remove those dependencies, which I published to Gist Host using the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uvx claude-code-transcripts web --gist
&lt;/code&gt;&lt;/pre&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/github"&gt;github&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cors"&gt;cors&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="github"/><category term="http"/><category term="javascript"/><category term="projects"/><category term="ai-assisted-programming"/><category term="cors"/></entry><entry><title>HTTP Range Requests for Wheel Metadata</title><link href="https://simonwillison.net/2025/Dec/26/http-range-wheel-metadata/#atom-tag" rel="alternate"/><published>2025-12-26T23:29:00+00:00</published><updated>2025-12-26T23:29:00+00:00</updated><id>https://simonwillison.net/2025/Dec/26/http-range-wheel-metadata/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Research:&lt;/strong&gt; &lt;a href="https://github.com/simonw/research/tree/main/http-range-wheel-metadata#readme"&gt;HTTP Range Requests for Wheel Metadata&lt;/a&gt;&lt;/p&gt;
        &lt;p&gt;&lt;a href="https://simonwillison.net/2025/Dec/26/how-uv-got-so-fast/"&gt;More details here&lt;/a&gt;.&lt;/p&gt;
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http-range-requests"&gt;http-range-requests&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="http"/><category term="http-range-requests"/></entry><entry><title>YouTube embeds fail with a 153 error</title><link href="https://simonwillison.net/2025/Dec/1/youtube-embed-153-error/#atom-tag" rel="alternate"/><published>2025-12-01T05:26:23+00:00</published><updated>2025-12-01T05:26:23+00:00</updated><id>https://simonwillison.net/2025/Dec/1/youtube-embed-153-error/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/simonwillisonblog/issues/561"&gt;YouTube embeds fail with a 153 error&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I just fixed this bug on my blog. I was getting an annoying "Error 153: Video player configuration error" on some of the YouTube video embeds (like &lt;a href="https://simonwillison.net/2024/Jun/21/search-based-rag/"&gt;this one&lt;/a&gt;) on this site. After some digging it turns out the culprit was this HTTP header, which Django's SecurityMiddleware was &lt;a href="https://docs.djangoproject.com/en/5.2/ref/middleware/#module-django.middleware.security"&gt;sending by default&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Referrer-Policy: same-origin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;YouTube's &lt;a href="https://developers.google.com/youtube/terms/required-minimum-functionality#embedded-player-api-client-identity"&gt;embedded player terms documentation&lt;/a&gt; explains why this broke:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;API Clients that use the YouTube embedded player (including the YouTube IFrame Player API) must provide identification through the &lt;code&gt;HTTP Referer&lt;/code&gt; request header. In some environments, the browser will automatically set &lt;code&gt;HTTP Referer&lt;/code&gt;, and API Clients need only ensure they are not setting the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Referrer-Policy"&gt;&lt;code&gt;Referrer-Policy&lt;/code&gt;&lt;/a&gt; in a way that suppresses the &lt;code&gt;Referer&lt;/code&gt; value. YouTube recommends using &lt;code&gt;strict-origin-when-cross-origin&lt;/code&gt; Referrer-Policy, which is already the default in many browsers.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The fix, which I &lt;a href="https://github.com/simonw/simonwillisonblog/pull/562"&gt;outsourced to GitHub Copilot agent&lt;/a&gt; since I was on my phone, was to add this to my &lt;code&gt;settings.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This &lt;a href="https://developer.chrome.com/blog/referrer-policy-new-chrome-default"&gt;explainer on the Chrome blog&lt;/a&gt; describes what the header means:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;strict-origin-when-cross-origin&lt;/code&gt; offers more privacy. With this policy, only the origin is sent in the Referer header of cross-origin requests.&lt;/p&gt;
&lt;p&gt;This prevents leaks of private data that may be accessible from other parts of the full URL such as the path and query string.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Effectively it means that any time you follow a link from my site to somewhere else they'll see this in the incoming HTTP headers even if you followed the link from a page other than my homepage:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Referer: https://simonwillison.net/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The previous header, &lt;code&gt;same-origin&lt;/code&gt;, is &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Referrer-Policy"&gt;explained by MDN here&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Send the &lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/Origin"&gt;origin&lt;/a&gt;, path, and query string for &lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/Same-origin_policy"&gt;same-origin&lt;/a&gt; requests. Don't send the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Referer"&gt;&lt;code&gt;Referer&lt;/code&gt;&lt;/a&gt; header for cross-origin requests.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This meant that previously traffic from my site wasn't sending any HTTP referer at all!


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/privacy"&gt;privacy&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/youtube"&gt;youtube&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="http"/><category term="privacy"/><category term="youtube"/></entry><entry><title>httpjail</title><link href="https://simonwillison.net/2025/Sep/19/httpjail/#atom-tag" rel="alternate"/><published>2025-09-19T21:57:29+00:00</published><updated>2025-09-19T21:57:29+00:00</updated><id>https://simonwillison.net/2025/Sep/19/httpjail/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/coder/httpjail"&gt;httpjail&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Here's a promising new (experimental) project in the sandboxing space from Ammar Bandukwala at &lt;a href="https://coder.com/"&gt;Coder&lt;/a&gt;. &lt;code&gt;httpjail&lt;/code&gt; provides a Rust CLI tool for running an individual process against a custom configured HTTP proxy.&lt;/p&gt;
&lt;p&gt;The initial goal is to help run coding agents like Claude Code and Codex CLI with extra rules governing how they interact with outside services. From Ammar's blog post that introduces the new tool, &lt;a href="https://ammar.io/blog/httpjail"&gt;Fine-grained HTTP filtering for Claude Code&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;httpjail&lt;/code&gt; implements an HTTP(S) interceptor alongside process-level network isolation. Under default configuration, all DNS (udp:53) is permitted and all other non-HTTP(S) traffic is blocked.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;httpjail&lt;/code&gt; rules are either JavaScript expressions or custom programs. This approach makes them far more flexible than traditional rule-oriented firewalls and avoids the learning curve of a DSL.&lt;/p&gt;
&lt;p&gt;Block all HTTP requests other than the LLM API traffic itself:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ httpjail --js "r.host === 'api.anthropic.com'" -- claude "build something great"
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;I tried it out using OpenAI's Codex CLI instead and found this recipe worked:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;brew upgrade rust
cargo install httpjail # Drops it in `~/.cargo/bin`
httpjail --js "r.host === 'chatgpt.com'" -- codex
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Within that Codex instance the model ran fine but any attempts to access other URLs (e.g. telling it "&lt;code&gt;Use curl to fetch simonwillison.net&lt;/code&gt;)" failed at the proxy layer.&lt;/p&gt;
&lt;p&gt;This is still at a really early stage but there's a lot I like about this project. Being able to use JavaScript to filter requests via the &lt;code&gt;--js&lt;/code&gt; option is neat (it's using V8 under the hood), and there's also a &lt;code&gt;--sh shellscript&lt;/code&gt; option which instead runs a shell program passing environment variables that can be used to determine if the request should be allowed.&lt;/p&gt;
&lt;p&gt;At a basic level it works by running a proxy server and setting &lt;code&gt;HTTP_PROXY&lt;/code&gt; and &lt;code&gt;HTTPS_PROXY&lt;/code&gt; environment variables so well-behaving software knows how to route requests.&lt;/p&gt;
&lt;p&gt;It can also add a bunch of other layers. On Linux it sets up &lt;a href="https://en.wikipedia.org/wiki/Nftables"&gt;nftables&lt;/a&gt; rules to explicitly deny additional network access. There's also a &lt;code&gt;--docker-run&lt;/code&gt; option which can launch a Docker container with the specified image but first locks that container down to only have network access to the &lt;code&gt;httpjail&lt;/code&gt; proxy server.&lt;/p&gt;
&lt;p&gt;It can intercept, filter and log HTTPS requests too by generating its own certificate and making that available to the underlying process.&lt;/p&gt;
&lt;p&gt;I'm always interested in new approaches to sandboxing, and fine-grained network access is a particularly tricky problem to solve. This looks like a very promising step in that direction - I'm looking forward to seeing how this project continues to evolve.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://ammar.io/blog/httpjail"&gt;Fine-grained HTTP filtering for Claude Code&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/proxies"&gt;proxies&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sandboxing"&gt;sandboxing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/v8"&gt;v8&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rust"&gt;rust&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-code"&gt;claude-code&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/codex"&gt;codex&lt;/a&gt;&lt;/p&gt;



</summary><category term="http"/><category term="javascript"/><category term="proxies"/><category term="sandboxing"/><category term="security"/><category term="v8"/><category term="rust"/><category term="claude-code"/><category term="codex"/></entry><entry><title>tidwall/pogocache</title><link href="https://simonwillison.net/2025/Jul/21/pogocache/#atom-tag" rel="alternate"/><published>2025-07-21T23:58:53+00:00</published><updated>2025-07-21T23:58:53+00:00</updated><id>https://simonwillison.net/2025/Jul/21/pogocache/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/tidwall/pogocache"&gt;tidwall/pogocache&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
New project from Josh Baker, author of the excellent &lt;code&gt;tg&lt;/code&gt; C geospatial libarry (&lt;a href="https://simonwillison.net/2023/Sep/23/tg-polygon-indexing/"&gt;covered previously&lt;/a&gt;) and various other &lt;a href="https://github.com/tidwall"&gt;interesting projects&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Pogocache is fast caching software built from scratch with a focus on low latency and cpu efficency.&lt;/p&gt;
&lt;p&gt;Faster: Pogocache is faster than Memcache, Valkey, Redis, Dragonfly, and Garnet. It has the lowest latency per request, providing the quickest response times. It's optimized to scale from one to many cores, giving you the best single-threaded and multithreaded performance.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Faster than Memcache and Redis is a big claim! The README includes a &lt;a href="https://github.com/tidwall/pogocache/blob/main/README.md#design-details"&gt;design details&lt;/a&gt; section that explains how the system achieves that performance, using a sharded hashmap inspired by Josh's &lt;a href="https://github.com/tidwall/shardmap"&gt;shardmap&lt;/a&gt; project and clever application of threads.&lt;/p&gt;
&lt;p&gt;Performance aside, the most interesting thing about Pogocache is the server interface it provides: it emulates the APIs for Redis and Memcached, provides a simple HTTP API &lt;em&gt;and&lt;/em&gt; lets you talk to it over the PostgreSQL wire protocol as well!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;psql -h localhost -p 9401
=&amp;gt; SET first Tom;
=&amp;gt; SET last Anderson;
=&amp;gt; SET age 37;

$ curl http://localhost:9401/last
Anderson
&lt;/code&gt;&lt;/pre&gt;

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=44638076"&gt;Show HN&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/c"&gt;c&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/memcached"&gt;memcached&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/redis"&gt;redis&lt;/a&gt;&lt;/p&gt;



</summary><category term="c"/><category term="caching"/><category term="http"/><category term="memcached"/><category term="postgresql"/><category term="redis"/></entry><entry><title>Some Go web dev notes</title><link href="https://simonwillison.net/2024/Sep/27/some-go-web-dev-notes/#atom-tag" rel="alternate"/><published>2024-09-27T23:43:31+00:00</published><updated>2024-09-27T23:43:31+00:00</updated><id>https://simonwillison.net/2024/Sep/27/some-go-web-dev-notes/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://jvns.ca/blog/2024/09/27/some-go-web-dev-notes/"&gt;Some Go web dev notes&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Julia Evans on writing small, self-contained web applications in Go:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In general everything about it feels like it makes projects easy to work on for 5 days, abandon for 2 years, and then get back into writing code without a lot of problems.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Go 1.22 &lt;a href="https://go.dev/blog/routing-enhancements"&gt;introduced HTTP routing&lt;/a&gt; in February of this year, making it even more practical to build a web application using just the Go standard library.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/go"&gt;go&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/web-development"&gt;web-development&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/julia-evans"&gt;julia-evans&lt;/a&gt;&lt;/p&gt;



</summary><category term="go"/><category term="http"/><category term="web-development"/><category term="julia-evans"/></entry><entry><title>How streaming LLM APIs work</title><link href="https://simonwillison.net/2024/Sep/22/how-streaming-llm-apis-work/#atom-tag" rel="alternate"/><published>2024-09-22T03:48:12+00:00</published><updated>2024-09-22T03:48:12+00:00</updated><id>https://simonwillison.net/2024/Sep/22/how-streaming-llm-apis-work/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://til.simonwillison.net/llms/streaming-llm-apis"&gt;How streaming LLM APIs work&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
New TIL. I used &lt;code&gt;curl&lt;/code&gt; to explore the streaming APIs provided by OpenAI, Anthropic and Google Gemini and wrote up detailed notes on what I learned.&lt;/p&gt;
&lt;p&gt;Also includes example code for &lt;a href="https://til.simonwillison.net/llms/streaming-llm-apis#user-content-bonus-accessing-these-streams-using-httpx"&gt;receiving streaming events in Python with HTTPX&lt;/a&gt; and &lt;a href="https://til.simonwillison.net/llms/streaming-llm-apis#user-content-bonus--2-processing-streaming-events-in-javascript-with-fetch"&gt;receiving streaming events in client-side JavaScript using fetch()&lt;/a&gt;.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/apis"&gt;apis&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/json"&gt;json&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;&lt;/p&gt;



</summary><category term="apis"/><category term="http"/><category term="json"/><category term="llms"/></entry><entry><title>SQL Injection Isn't Dead: Smuggling Queries at the Protocol Level</title><link href="https://simonwillison.net/2024/Aug/12/smuggling-queries-at-the-protocol-level/#atom-tag" rel="alternate"/><published>2024-08-12T15:36:47+00:00</published><updated>2024-08-12T15:36:47+00:00</updated><id>https://simonwillison.net/2024/Aug/12/smuggling-queries-at-the-protocol-level/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://media.defcon.org/DEF%20CON%2032/DEF%20CON%2032%20presentations/DEF%20CON%2032%20-%20Paul%20Gerste%20-%20SQL%20Injection%20Isn%27t%20Dead%20Smuggling%20Queries%20at%20the%20Protocol%20Level.pdf"&gt;SQL Injection Isn&amp;#x27;t Dead: Smuggling Queries at the Protocol Level&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
PDF slides from a presentation by &lt;a href="https://twitter.com/pspaul95"&gt;Paul Gerste&lt;/a&gt; at DEF CON 32. It turns out some databases have vulnerabilities in their binary protocols that can be exploited by carefully crafted SQL queries.&lt;/p&gt;
&lt;p&gt;Paul demonstrates an attack against PostgreSQL (which works in some but not all of the PostgreSQL client libraries) which uses a message size overflow, by embedding a string longer than 4GB (2**32 bytes) which overflows the maximum length of a string in the underlying protocol and writes data to the subsequent value. He then shows a similar attack against MongoDB.&lt;/p&gt;
&lt;p&gt;The current way to protect against these attacks is to ensure a size limit on incoming requests. This can be more difficult than you may expect - Paul points out that alternative paths such as WebSockets might bypass limits that are in place for regular HTTP requests, plus some servers may apply limits before decompression, allowing an attacker to send a compressed payload that is larger than the configured limit.&lt;/p&gt;
&lt;p&gt;&lt;img alt="How Web Apps Handle Large Payloads. Potential bypasses: - Unprotected endpoints - Compression - WebSockets (highlighted) - Alternate body types - Incrementation.  Next to WebSockets:  - Compression support - Large message size - Many filters don't apply" src="https://static.simonwillison.net/static/2024/sql-injection-websockets.jpg" /&gt;

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://lobste.rs/s/mxgp7v/sql_injection_isn_t_dead_smuggling"&gt;lobste.rs&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mongodb"&gt;mongodb&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sql-injection"&gt;sql-injection&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/websockets"&gt;websockets&lt;/a&gt;&lt;/p&gt;



</summary><category term="http"/><category term="mongodb"/><category term="postgresql"/><category term="security"/><category term="sql-injection"/><category term="websockets"/></entry><entry><title>django-http-debug, a new Django app mostly written by Claude</title><link href="https://simonwillison.net/2024/Aug/8/django-http-debug/#atom-tag" rel="alternate"/><published>2024-08-08T15:26:27+00:00</published><updated>2024-08-08T15:26:27+00:00</updated><id>https://simonwillison.net/2024/Aug/8/django-http-debug/#atom-tag</id><summary type="html">
    &lt;p&gt;Yesterday I finally developed something I’ve been casually thinking about building for a long time: &lt;strong&gt;&lt;a href="https://github.com/simonw/django-http-debug"&gt;django-http-debug&lt;/a&gt;&lt;/strong&gt;. It’s a reusable Django app - something you can &lt;code&gt;pip install&lt;/code&gt; into any Django project - which provides tools for quickly setting up a URL that returns a canned HTTP response and logs the full details of any incoming request to a database table.&lt;/p&gt;
&lt;p&gt;This is ideal for any time you want to start developing against some external API that sends traffic to your own site - a webhooks provider &lt;a href="https://docs.stripe.com/webhooks"&gt;like Stripe&lt;/a&gt;, or an OAuth or OpenID connect integration (my task yesterday morning).&lt;/p&gt;
&lt;p&gt;You can install it right now in your own Django app: add &lt;code&gt;django-http-debug&lt;/code&gt; to your requirements (or just &lt;code&gt;pip install django-http-debug&lt;/code&gt;), then add the following to your &lt;code&gt;settings.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-v"&gt;INSTALLED_APPS&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; [
    &lt;span class="pl-c"&gt;# ...&lt;/span&gt;
    &lt;span class="pl-s"&gt;'django_http_debug'&lt;/span&gt;,
    &lt;span class="pl-c"&gt;# ...&lt;/span&gt;
]

&lt;span class="pl-v"&gt;MIDDLEWARE&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; [
    &lt;span class="pl-c"&gt;# ...&lt;/span&gt;
    &lt;span class="pl-s"&gt;"django_http_debug.middleware.DebugMiddleware"&lt;/span&gt;,
    &lt;span class="pl-c"&gt;# ...&lt;/span&gt;
]&lt;/pre&gt;
&lt;p&gt;You'll need to have the Django Admin app configured as well. The result will be two new models managed by the admin - one for endpoints:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/django-http-debug-add-endpoint-2.jpg" alt="Django admin screenshot: add debug endpoint. Path is set to hello-world, status code is 200, content-type is text/plain; charset=utf-8, headers is {&amp;quot;x-hello&amp;quot;: &amp;quot;world&amp;quot;}, content is Hello world, The is base 64 checkbox is blank and the logging enabled checkbox is checked." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;And a read-only model for viewing logged requests:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/django-http-debug-logs.jpg" alt="Django admin screenshot showing a list of three logged requests to the hello-world endpoint, all three have a timestamp, method and query string - the method is GET for them all but the query string is blank for one, a=b for another and c=d for a third." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;It’s possible to disable logging for an endpoint, which means &lt;code&gt;django-http-debug&lt;/code&gt; doubles as a tool for adding things like a &lt;code&gt;robots.txt&lt;/code&gt; to your site without needing to deploy any additional code.&lt;/p&gt;
&lt;h4 id="how-it-works"&gt;How it works&lt;/h4&gt;
&lt;p&gt;The key to how this works is &lt;a href="https://github.com/simonw/django-http-debug/blob/0.2/django_http_debug/middleware.py"&gt;this piece of middleware&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;class&lt;/span&gt; &lt;span class="pl-v"&gt;DebugMiddleware&lt;/span&gt;:
    &lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;__init__&lt;/span&gt;(&lt;span class="pl-s1"&gt;self&lt;/span&gt;, &lt;span class="pl-s1"&gt;get_response&lt;/span&gt;):
        &lt;span class="pl-s1"&gt;self&lt;/span&gt;.&lt;span class="pl-s1"&gt;get_response&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;get_response&lt;/span&gt;

    &lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;__call__&lt;/span&gt;(&lt;span class="pl-s1"&gt;self&lt;/span&gt;, &lt;span class="pl-s1"&gt;request&lt;/span&gt;):
        &lt;span class="pl-s1"&gt;response&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;self&lt;/span&gt;.&lt;span class="pl-en"&gt;get_response&lt;/span&gt;(&lt;span class="pl-s1"&gt;request&lt;/span&gt;)
        &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s1"&gt;response&lt;/span&gt;.&lt;span class="pl-s1"&gt;status_code&lt;/span&gt; &lt;span class="pl-c1"&gt;==&lt;/span&gt; &lt;span class="pl-c1"&gt;404&lt;/span&gt;:
            &lt;span class="pl-s1"&gt;path&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;request&lt;/span&gt;.&lt;span class="pl-s1"&gt;path&lt;/span&gt;.&lt;span class="pl-en"&gt;lstrip&lt;/span&gt;(&lt;span class="pl-s"&gt;"/"&lt;/span&gt;)
            &lt;span class="pl-s1"&gt;debug_response&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;debug_view&lt;/span&gt;(&lt;span class="pl-s1"&gt;request&lt;/span&gt;, &lt;span class="pl-s1"&gt;path&lt;/span&gt;)
            &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s1"&gt;debug_response&lt;/span&gt;:
                &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-s1"&gt;debug_response&lt;/span&gt;
        &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-s1"&gt;response&lt;/span&gt;&lt;/pre&gt;
&lt;p&gt;This dispatches to the default &lt;code&gt;get_response()&lt;/code&gt; function, then intercepts the result and checks if it's a 404. If so, it gives the &lt;code&gt;debug_view()&lt;/code&gt; function an opportunity to respond instead - which might return &lt;code&gt;None&lt;/code&gt;, in which case that original 404 is returned to the client.&lt;/p&gt;
&lt;p&gt;That &lt;code&gt;debug_view()&lt;/code&gt; function &lt;a href="https://github.com/simonw/django-http-debug/blob/0.2/django_http_debug/views.py"&gt;looks like this&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-en"&gt;@&lt;span class="pl-s1"&gt;csrf_exempt&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;debug_view&lt;/span&gt;(&lt;span class="pl-s1"&gt;request&lt;/span&gt;, &lt;span class="pl-s1"&gt;path&lt;/span&gt;):
    &lt;span class="pl-k"&gt;try&lt;/span&gt;:
        &lt;span class="pl-s1"&gt;endpoint&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-v"&gt;DebugEndpoint&lt;/span&gt;.&lt;span class="pl-s1"&gt;objects&lt;/span&gt;.&lt;span class="pl-en"&gt;get&lt;/span&gt;(&lt;span class="pl-s1"&gt;path&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;path&lt;/span&gt;)
    &lt;span class="pl-k"&gt;except&lt;/span&gt; &lt;span class="pl-v"&gt;DebugEndpoint&lt;/span&gt;.&lt;span class="pl-v"&gt;DoesNotExist&lt;/span&gt;:
        &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-c1"&gt;None&lt;/span&gt;  &lt;span class="pl-c"&gt;# Allow normal 404 handling to continue&lt;/span&gt;

    &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s1"&gt;endpoint&lt;/span&gt;.&lt;span class="pl-s1"&gt;logging_enabled&lt;/span&gt;:
        &lt;span class="pl-s1"&gt;log_entry&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-v"&gt;RequestLog&lt;/span&gt;(
            &lt;span class="pl-s1"&gt;endpoint&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;endpoint&lt;/span&gt;,
            &lt;span class="pl-s1"&gt;method&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;request&lt;/span&gt;.&lt;span class="pl-s1"&gt;method&lt;/span&gt;,
            &lt;span class="pl-s1"&gt;query_string&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;request&lt;/span&gt;.&lt;span class="pl-v"&gt;META&lt;/span&gt;.&lt;span class="pl-en"&gt;get&lt;/span&gt;(&lt;span class="pl-s"&gt;"QUERY_STRING"&lt;/span&gt;, &lt;span class="pl-s"&gt;""&lt;/span&gt;),
            &lt;span class="pl-s1"&gt;headers&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-en"&gt;dict&lt;/span&gt;(&lt;span class="pl-s1"&gt;request&lt;/span&gt;.&lt;span class="pl-s1"&gt;headers&lt;/span&gt;),
        )
        &lt;span class="pl-s1"&gt;log_entry&lt;/span&gt;.&lt;span class="pl-en"&gt;set_body&lt;/span&gt;(&lt;span class="pl-s1"&gt;request&lt;/span&gt;.&lt;span class="pl-s1"&gt;body&lt;/span&gt;)
        &lt;span class="pl-s1"&gt;log_entry&lt;/span&gt;.&lt;span class="pl-en"&gt;save&lt;/span&gt;()

    &lt;span class="pl-s1"&gt;content&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;endpoint&lt;/span&gt;.&lt;span class="pl-s1"&gt;content&lt;/span&gt;
    &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s1"&gt;endpoint&lt;/span&gt;.&lt;span class="pl-s1"&gt;is_base64&lt;/span&gt;:
        &lt;span class="pl-s1"&gt;content&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;base64&lt;/span&gt;.&lt;span class="pl-en"&gt;b64decode&lt;/span&gt;(&lt;span class="pl-s1"&gt;content&lt;/span&gt;)

    &lt;span class="pl-s1"&gt;response&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-v"&gt;HttpResponse&lt;/span&gt;(
        &lt;span class="pl-s1"&gt;content&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;content&lt;/span&gt;,
        &lt;span class="pl-s1"&gt;status&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;endpoint&lt;/span&gt;.&lt;span class="pl-s1"&gt;status_code&lt;/span&gt;,
        &lt;span class="pl-s1"&gt;content_type&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;endpoint&lt;/span&gt;.&lt;span class="pl-s1"&gt;content_type&lt;/span&gt;,
    )
    &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;key&lt;/span&gt;, &lt;span class="pl-s1"&gt;value&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;endpoint&lt;/span&gt;.&lt;span class="pl-s1"&gt;headers&lt;/span&gt;.&lt;span class="pl-en"&gt;items&lt;/span&gt;():
        &lt;span class="pl-s1"&gt;response&lt;/span&gt;[&lt;span class="pl-s1"&gt;key&lt;/span&gt;] &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;value&lt;/span&gt;

    &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-s1"&gt;response&lt;/span&gt;&lt;/pre&gt;
&lt;p&gt;It checks the database for an endpoint matching the incoming path, then logs the response (if the endpoint has &lt;code&gt;logging_enabled&lt;/code&gt; set) and returns a canned response based on the endpoint configuration.&lt;/p&gt;
&lt;p&gt;Here are the &lt;a href="https://github.com/simonw/django-http-debug/blob/0.2/django_http_debug/models.py"&gt;models&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;django&lt;/span&gt;.&lt;span class="pl-s1"&gt;db&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;models&lt;/span&gt;
&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;base64&lt;/span&gt;


&lt;span class="pl-k"&gt;class&lt;/span&gt; &lt;span class="pl-v"&gt;DebugEndpoint&lt;/span&gt;(&lt;span class="pl-s1"&gt;models&lt;/span&gt;.&lt;span class="pl-v"&gt;Model&lt;/span&gt;):
    &lt;span class="pl-s1"&gt;path&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;models&lt;/span&gt;.&lt;span class="pl-v"&gt;CharField&lt;/span&gt;(&lt;span class="pl-s1"&gt;max_length&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;255&lt;/span&gt;, &lt;span class="pl-s1"&gt;unique&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;)
    &lt;span class="pl-s1"&gt;status_code&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;models&lt;/span&gt;.&lt;span class="pl-v"&gt;IntegerField&lt;/span&gt;(&lt;span class="pl-s1"&gt;default&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;200&lt;/span&gt;)
    &lt;span class="pl-s1"&gt;content_type&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;models&lt;/span&gt;.&lt;span class="pl-v"&gt;CharField&lt;/span&gt;(&lt;span class="pl-s1"&gt;max_length&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;64&lt;/span&gt;, &lt;span class="pl-s1"&gt;default&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"text/plain; charset=utf-8"&lt;/span&gt;)
    &lt;span class="pl-s1"&gt;headers&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;models&lt;/span&gt;.&lt;span class="pl-v"&gt;JSONField&lt;/span&gt;(&lt;span class="pl-s1"&gt;default&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;dict&lt;/span&gt;, &lt;span class="pl-s1"&gt;blank&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;)
    &lt;span class="pl-s1"&gt;content&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;models&lt;/span&gt;.&lt;span class="pl-v"&gt;TextField&lt;/span&gt;(&lt;span class="pl-s1"&gt;blank&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;)
    &lt;span class="pl-s1"&gt;is_base64&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;models&lt;/span&gt;.&lt;span class="pl-v"&gt;BooleanField&lt;/span&gt;(&lt;span class="pl-s1"&gt;default&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;False&lt;/span&gt;)
    &lt;span class="pl-s1"&gt;logging_enabled&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;models&lt;/span&gt;.&lt;span class="pl-v"&gt;BooleanField&lt;/span&gt;(&lt;span class="pl-s1"&gt;default&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;)

    &lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;__str__&lt;/span&gt;(&lt;span class="pl-s1"&gt;self&lt;/span&gt;):
        &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-s1"&gt;self&lt;/span&gt;.&lt;span class="pl-s1"&gt;path&lt;/span&gt;

    &lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;get_absolute_url&lt;/span&gt;(&lt;span class="pl-s1"&gt;self&lt;/span&gt;):
        &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-s"&gt;f"/&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;&lt;span class="pl-s1"&gt;self&lt;/span&gt;.&lt;span class="pl-s1"&gt;path&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;"&lt;/span&gt;


&lt;span class="pl-k"&gt;class&lt;/span&gt; &lt;span class="pl-v"&gt;RequestLog&lt;/span&gt;(&lt;span class="pl-s1"&gt;models&lt;/span&gt;.&lt;span class="pl-v"&gt;Model&lt;/span&gt;):
    &lt;span class="pl-s1"&gt;endpoint&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;models&lt;/span&gt;.&lt;span class="pl-v"&gt;ForeignKey&lt;/span&gt;(&lt;span class="pl-v"&gt;DebugEndpoint&lt;/span&gt;, &lt;span class="pl-s1"&gt;on_delete&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;models&lt;/span&gt;.&lt;span class="pl-v"&gt;CASCADE&lt;/span&gt;)
    &lt;span class="pl-s1"&gt;method&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;models&lt;/span&gt;.&lt;span class="pl-v"&gt;CharField&lt;/span&gt;(&lt;span class="pl-s1"&gt;max_length&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;10&lt;/span&gt;)
    &lt;span class="pl-s1"&gt;query_string&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;models&lt;/span&gt;.&lt;span class="pl-v"&gt;CharField&lt;/span&gt;(&lt;span class="pl-s1"&gt;max_length&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;255&lt;/span&gt;, &lt;span class="pl-s1"&gt;blank&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;)
    &lt;span class="pl-s1"&gt;headers&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;models&lt;/span&gt;.&lt;span class="pl-v"&gt;JSONField&lt;/span&gt;()
    &lt;span class="pl-s1"&gt;body&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;models&lt;/span&gt;.&lt;span class="pl-v"&gt;TextField&lt;/span&gt;(&lt;span class="pl-s1"&gt;blank&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;)
    &lt;span class="pl-s1"&gt;is_base64&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;models&lt;/span&gt;.&lt;span class="pl-v"&gt;BooleanField&lt;/span&gt;(&lt;span class="pl-s1"&gt;default&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;False&lt;/span&gt;)
    &lt;span class="pl-s1"&gt;timestamp&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;models&lt;/span&gt;.&lt;span class="pl-v"&gt;DateTimeField&lt;/span&gt;(&lt;span class="pl-s1"&gt;auto_now_add&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;)

    &lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;__str__&lt;/span&gt;(&lt;span class="pl-s1"&gt;self&lt;/span&gt;):
        &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-s"&gt;f"&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;&lt;span class="pl-s1"&gt;self&lt;/span&gt;.&lt;span class="pl-s1"&gt;method&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;&lt;span class="pl-s1"&gt;self&lt;/span&gt;.&lt;span class="pl-s1"&gt;endpoint&lt;/span&gt;.&lt;span class="pl-s1"&gt;path&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt; at &lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;&lt;span class="pl-s1"&gt;self&lt;/span&gt;.&lt;span class="pl-s1"&gt;timestamp&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;"&lt;/span&gt;

    &lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;set_body&lt;/span&gt;(&lt;span class="pl-s1"&gt;self&lt;/span&gt;, &lt;span class="pl-s1"&gt;body&lt;/span&gt;):
        &lt;span class="pl-k"&gt;try&lt;/span&gt;:
            &lt;span class="pl-c"&gt;# Try to decode as UTF-8&lt;/span&gt;
            &lt;span class="pl-s1"&gt;self&lt;/span&gt;.&lt;span class="pl-s1"&gt;body&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;body&lt;/span&gt;.&lt;span class="pl-en"&gt;decode&lt;/span&gt;(&lt;span class="pl-s"&gt;"utf-8"&lt;/span&gt;)
            &lt;span class="pl-s1"&gt;self&lt;/span&gt;.&lt;span class="pl-s1"&gt;is_base64&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;False&lt;/span&gt;
        &lt;span class="pl-k"&gt;except&lt;/span&gt; &lt;span class="pl-v"&gt;UnicodeDecodeError&lt;/span&gt;:
            &lt;span class="pl-c"&gt;# If that fails, store as base64&lt;/span&gt;
            &lt;span class="pl-s1"&gt;self&lt;/span&gt;.&lt;span class="pl-s1"&gt;body&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;base64&lt;/span&gt;.&lt;span class="pl-en"&gt;b64encode&lt;/span&gt;(&lt;span class="pl-s1"&gt;body&lt;/span&gt;).&lt;span class="pl-en"&gt;decode&lt;/span&gt;(&lt;span class="pl-s"&gt;"ascii"&lt;/span&gt;)
            &lt;span class="pl-s1"&gt;self&lt;/span&gt;.&lt;span class="pl-s1"&gt;is_base64&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;True&lt;/span&gt;

    &lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;get_body&lt;/span&gt;(&lt;span class="pl-s1"&gt;self&lt;/span&gt;):
        &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s1"&gt;self&lt;/span&gt;.&lt;span class="pl-s1"&gt;is_base64&lt;/span&gt;:
            &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-s1"&gt;base64&lt;/span&gt;.&lt;span class="pl-en"&gt;b64decode&lt;/span&gt;(&lt;span class="pl-s1"&gt;self&lt;/span&gt;.&lt;span class="pl-s1"&gt;body&lt;/span&gt;.&lt;span class="pl-en"&gt;encode&lt;/span&gt;(&lt;span class="pl-s"&gt;"ascii"&lt;/span&gt;))
        &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-s1"&gt;self&lt;/span&gt;.&lt;span class="pl-s1"&gt;body&lt;/span&gt;&lt;/pre&gt;
&lt;p&gt;The admin screens are defined in &lt;a href="https://github.com/simonw/django-http-debug/blob/0.2/django_http_debug/admin.py"&gt;admin.py&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="claude-built-the-first-version-of-this-for-me"&gt;Claude built the first version of this for me&lt;/h4&gt;
&lt;p&gt;This is a classic example of a project that I couldn’t quite justify building without assistance from an LLM. I wanted it to exist, but I didn't want to spend a whole day building it.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://simonwillison.net/2024/Jun/20/claude-35-sonnet/"&gt;Claude 3.5 Sonnet&lt;/a&gt; got me 90% of the way to a working first version. I had to make a few tweaks to how the middleware worked, but having done that I had a working initial prototype within a few minutes of starting the project.&lt;/p&gt;
&lt;p&gt;Here’s the full sequence of prompts I used, each linking to the code that was produced for me (as a Claude artifact):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I want a Django app I can use to help create HTTP debugging endpoints. It should let me configure a new path e.g. /webhooks/receive/ that the Django 404 handler then hooks into - if one is configured it can be told which HTTP status code, headers and content to return.&lt;/p&gt;
&lt;p&gt;ALL traffic to those endpoints is logged to a Django table - full details of incoming request headers, method and body. Those can be browsed read-only in the Django admin (and deleted)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Produced &lt;a href="https://claude.site/artifacts/d7da92c2-8a6e-4fd8-a6f2-b243523af1b4"&gt;Claude v1&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;make it so I don't have to put it in the urlpatterns because it hooks ito Django's 404 handling mechanism instead&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Produced &lt;a href="https://claude.site/artifacts/a1fb7996-e16b-403f-848c-e9ff0adcb9e3"&gt;Claude v2&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Suggestions for how this could handle request bodies that don't cleanly decode to utf-8&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Produced &lt;a href="https://claude.site/artifacts/9f1a2db7-d614-4fc0-9c84-860a2c1afa92"&gt;Claude v3&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;don't use a binary field, use a text field but still store base64 data in it if necessary and have a is_base64 boolean column that gets set to true if that happens&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Produced &lt;a href="https://claude.site/artifacts/c49367b9-b6f9-4634-be72-a266e01579fd"&gt;Claude v4&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I took that code and ran with it - I fired up a new skeleton library using my &lt;a href="https://github.com/simonw/python-lib"&gt;python-lib cookiecutter template&lt;/a&gt;, copied the code into it, made some tiny changes to get it to work and shipped it as &lt;a href="https://github.com/simonw/django-http-debug/releases/tag/0.1a0"&gt;an initial alpha release&lt;/a&gt; - mainly so I could start exercising it on a couple of sites I manage.&lt;/p&gt;
&lt;p&gt;Using it in the wild for a few minutes quickly identified changes I needed to make. I filed those as &lt;a href="https://github.com/simonw/django-http-debug/issues"&gt;issues&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/django-http-debug/issues/2"&gt;#2: Capture query string&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/django-http-debug/issues/3"&gt;#3: Don't show body field twice&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/django-http-debug/issues/4"&gt;#4: Field for content-type, plus base64 support&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/django-http-debug/issues/5"&gt;#5: Ability to disable logging for an endpoint&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/django-http-debug/issues/6"&gt;#6: Add automated tests&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then I worked though fixing each of those one at a time. I did most of this work myself, though GitHub Copilot helped me out be typing some of the code for me.&lt;/p&gt;
&lt;h4 id="adding-the-base64-preview"&gt;Adding the base64 preview&lt;/h4&gt;
&lt;p&gt;There was one slightly tricky feature I wanted to add that didn’t justify spending much time on but was absolutely a nice-to-have.&lt;/p&gt;
&lt;p&gt;The logging mechanism supports binary data: if incoming request data doesn’t cleanly encode as UTF-8 it gets stored as Base 64 text instead, with the &lt;code&gt;is_base64&lt;/code&gt; flag set to &lt;code&gt;True&lt;/code&gt; (see the &lt;code&gt;set_body()&lt;/code&gt; method in the &lt;code&gt;RequestLog&lt;/code&gt; model above).&lt;/p&gt;
&lt;p&gt;I asked Claude for a &lt;code&gt;curl&lt;/code&gt; one-liner to test this and it suggested:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;curl -X POST http://localhost:8000/foo/ \
  -H &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Content-Type: multipart/form-data&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; \
  -F &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;image=@pixel.gif&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I do this a lot - knocking out quick &lt;code&gt;curl&lt;/code&gt; commands is an easy prompt, and you can tell it the URL and headers you want to use, saving you from having to edit the command yourself later on.&lt;/p&gt;
&lt;p&gt;I decided to have the Django Admin view display a decoded version of that Base 64 data. But how to render that, when things like binary file uploads may not be cleanly renderable as text?&lt;/p&gt;
&lt;p&gt;This is what I came up with:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/django-http-debug-binary.jpg" alt="Django admin screenshot showing &amp;quot;view request log&amp;quot; screen - a logged POST request to the hello-world endpoint. method is POST, headers is a detailed dictionary, Body is a base64 string but body display shows that decoded to a multi-part form data with a image/gif attachment - that starts with GIF89a and then shows hex byte pairs for the binary data. Is base64 shows a green checkmark." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;The trick here I'm using here is to display the decoded data as a mix between renderable characters and hex byte pairs, with those pairs rendered using a different font to make it clear that they are part of the binary data.&lt;/p&gt;
&lt;p&gt;This is achieved using a &lt;code&gt;body_display()&lt;/code&gt; method on the &lt;code&gt;RequestLogAdmin&lt;/code&gt; admin class, which is then listed in &lt;code&gt;readonly_fields&lt;/code&gt;. The &lt;a href="https://github.com/simonw/django-http-debug/blob/0.2/django_http_debug/admin.py"&gt;full code is here&lt;/a&gt;, this is that method:&lt;/p&gt;
&lt;pre&gt;    &lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;body_display&lt;/span&gt;(&lt;span class="pl-s1"&gt;self&lt;/span&gt;, &lt;span class="pl-s1"&gt;obj&lt;/span&gt;):
        &lt;span class="pl-s1"&gt;body&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;obj&lt;/span&gt;.&lt;span class="pl-en"&gt;get_body&lt;/span&gt;()
        &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-c1"&gt;not&lt;/span&gt; &lt;span class="pl-en"&gt;isinstance&lt;/span&gt;(&lt;span class="pl-s1"&gt;body&lt;/span&gt;, &lt;span class="pl-s1"&gt;bytes&lt;/span&gt;):
            &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-en"&gt;format_html&lt;/span&gt;(&lt;span class="pl-s"&gt;"&amp;lt;pre&amp;gt;{}&amp;lt;/pre&amp;gt;"&lt;/span&gt;, &lt;span class="pl-s1"&gt;body&lt;/span&gt;)

        &lt;span class="pl-c"&gt;# Attempt to guess filetype&lt;/span&gt;
        &lt;span class="pl-s1"&gt;suggestion&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;None&lt;/span&gt;
        &lt;span class="pl-s1"&gt;match&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;filetype&lt;/span&gt;.&lt;span class="pl-en"&gt;guess&lt;/span&gt;(&lt;span class="pl-s1"&gt;body&lt;/span&gt;[:&lt;span class="pl-c1"&gt;1000&lt;/span&gt;])
        &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s1"&gt;match&lt;/span&gt;:
            &lt;span class="pl-s1"&gt;suggestion&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;"{} ({})"&lt;/span&gt;.&lt;span class="pl-en"&gt;format&lt;/span&gt;(&lt;span class="pl-s1"&gt;match&lt;/span&gt;.&lt;span class="pl-s1"&gt;extension&lt;/span&gt;, &lt;span class="pl-s1"&gt;match&lt;/span&gt;.&lt;span class="pl-s1"&gt;mime&lt;/span&gt;)

        &lt;span class="pl-s1"&gt;encoded&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;repr&lt;/span&gt;(&lt;span class="pl-s1"&gt;body&lt;/span&gt;)
        &lt;span class="pl-c"&gt;# Ditch the b' and trailing '&lt;/span&gt;
        &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s1"&gt;encoded&lt;/span&gt;.&lt;span class="pl-en"&gt;startswith&lt;/span&gt;(&lt;span class="pl-s"&gt;"b'"&lt;/span&gt;) &lt;span class="pl-c1"&gt;and&lt;/span&gt; &lt;span class="pl-s1"&gt;encoded&lt;/span&gt;.&lt;span class="pl-en"&gt;endswith&lt;/span&gt;(&lt;span class="pl-s"&gt;"'"&lt;/span&gt;):
            &lt;span class="pl-s1"&gt;encoded&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;encoded&lt;/span&gt;[&lt;span class="pl-c1"&gt;2&lt;/span&gt;:&lt;span class="pl-c1"&gt;-&lt;/span&gt;&lt;span class="pl-c1"&gt;1&lt;/span&gt;]

        &lt;span class="pl-c"&gt;# Split it into sequences of octets and characters&lt;/span&gt;
        &lt;span class="pl-s1"&gt;chunks&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;sequence_re&lt;/span&gt;.&lt;span class="pl-en"&gt;split&lt;/span&gt;(&lt;span class="pl-s1"&gt;encoded&lt;/span&gt;)
        &lt;span class="pl-s1"&gt;html&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; []
        &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s1"&gt;suggestion&lt;/span&gt;:
            &lt;span class="pl-s1"&gt;html&lt;/span&gt;.&lt;span class="pl-en"&gt;append&lt;/span&gt;(
                &lt;span class="pl-s"&gt;'&amp;lt;p style="margin-top: 0; font-family: monospace; font-size: 0.8em;"&amp;gt;Suggestion: {}&amp;lt;/p&amp;gt;'&lt;/span&gt;.&lt;span class="pl-en"&gt;format&lt;/span&gt;(
                    &lt;span class="pl-s1"&gt;suggestion&lt;/span&gt;
                )
            )
        &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;chunk&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;chunks&lt;/span&gt;:
            &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s1"&gt;sequence_re&lt;/span&gt;.&lt;span class="pl-en"&gt;match&lt;/span&gt;(&lt;span class="pl-s1"&gt;chunk&lt;/span&gt;):
                &lt;span class="pl-s1"&gt;octets&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;octet_re&lt;/span&gt;.&lt;span class="pl-en"&gt;findall&lt;/span&gt;(&lt;span class="pl-s1"&gt;chunk&lt;/span&gt;)
                &lt;span class="pl-s1"&gt;octets&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; [&lt;span class="pl-s1"&gt;o&lt;/span&gt;[&lt;span class="pl-c1"&gt;2&lt;/span&gt;:] &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;o&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;octets&lt;/span&gt;]
                &lt;span class="pl-s1"&gt;html&lt;/span&gt;.&lt;span class="pl-en"&gt;append&lt;/span&gt;(
                    &lt;span class="pl-s"&gt;'&amp;lt;code style="color: #999; font-family: monospace"&amp;gt;{}&amp;lt;/code&amp;gt;'&lt;/span&gt;.&lt;span class="pl-en"&gt;format&lt;/span&gt;(
                        &lt;span class="pl-s"&gt;" "&lt;/span&gt;.&lt;span class="pl-en"&gt;join&lt;/span&gt;(&lt;span class="pl-s1"&gt;octets&lt;/span&gt;).&lt;span class="pl-en"&gt;upper&lt;/span&gt;()
                    )
                )
            &lt;span class="pl-k"&gt;else&lt;/span&gt;:
                &lt;span class="pl-s1"&gt;html&lt;/span&gt;.&lt;span class="pl-en"&gt;append&lt;/span&gt;(&lt;span class="pl-s1"&gt;chunk&lt;/span&gt;.&lt;span class="pl-en"&gt;replace&lt;/span&gt;(&lt;span class="pl-s"&gt;"&lt;span class="pl-cce"&gt;\\&lt;/span&gt;&lt;span class="pl-cce"&gt;\\&lt;/span&gt;"&lt;/span&gt;, &lt;span class="pl-s"&gt;"&lt;span class="pl-cce"&gt;\\&lt;/span&gt;"&lt;/span&gt;))

        &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-en"&gt;mark_safe&lt;/span&gt;(&lt;span class="pl-s"&gt;" "&lt;/span&gt;.&lt;span class="pl-en"&gt;join&lt;/span&gt;(&lt;span class="pl-s1"&gt;html&lt;/span&gt;).&lt;span class="pl-en"&gt;strip&lt;/span&gt;().&lt;span class="pl-en"&gt;replace&lt;/span&gt;(&lt;span class="pl-s"&gt;"&lt;span class="pl-cce"&gt;\\&lt;/span&gt;r&lt;span class="pl-cce"&gt;\\&lt;/span&gt;n"&lt;/span&gt;, &lt;span class="pl-s"&gt;"&amp;lt;br&amp;gt;"&lt;/span&gt;))&lt;/pre&gt;
&lt;p&gt;I got Claude to write that using one of my favourite prompting tricks. I'd solved this problem once before in the past, &lt;a href="https://github.com/simonw/datasette-render-binary/blob/0.3.1/datasette_render_binary/__init__.py"&gt;in my datasette-render-binary&lt;/a&gt; project. So I pasted that code into Claude, told it:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;With that code as inspiration, modify the following Django Admin code to use that to display decoded base64 data:&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And then pasted in my existing Django admin class. You can see &lt;a href="https://gist.github.com/simonw/b2cfff8281d5681c30e54083a9882141"&gt;my full prompt here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Claude replied with &lt;a href="https://claude.site/artifacts/03454d25-9a1d-4b7d-b79f-a3a8707c58ad"&gt;this code&lt;/a&gt;, which almost worked exactly as intended - I had to make one change, swapping out the last line for this:&lt;/p&gt;
&lt;pre&gt;        &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-en"&gt;mark_safe&lt;/span&gt;(&lt;span class="pl-s"&gt;" "&lt;/span&gt;.&lt;span class="pl-en"&gt;join&lt;/span&gt;(&lt;span class="pl-s1"&gt;html&lt;/span&gt;).&lt;span class="pl-en"&gt;strip&lt;/span&gt;().&lt;span class="pl-en"&gt;replace&lt;/span&gt;(&lt;span class="pl-s"&gt;"&lt;span class="pl-cce"&gt;\\&lt;/span&gt;r&lt;span class="pl-cce"&gt;\\&lt;/span&gt;n"&lt;/span&gt;, &lt;span class="pl-s"&gt;"&amp;lt;br&amp;gt;"&lt;/span&gt;))&lt;/pre&gt;
&lt;p&gt;I love this pattern: "here's my existing code, here's some other code I wrote, combine them together to solve this problem". I wrote about this previously when I described &lt;a href="https://simonwillison.net/2024/Mar/30/ocr-pdfs-images/#ocr-how-i-built-this"&gt;how I built my PDF OCR JavaScript tool&lt;/a&gt; a few months ago.&lt;/p&gt;
&lt;h4 id="adding-automated-tests"&gt;Adding automated tests&lt;/h4&gt;
&lt;p&gt;The final challenge was the hardest: writing automated tests. This was difficult because Django tests need a full Django project configured for them, and I wasn’t confident about the best pattern for doing that in my standalone &lt;code&gt;django-http-debug&lt;/code&gt; repository since it wasn’t already part of an existing Django project.&lt;/p&gt;
&lt;p&gt;I decided to see if Claude could help me with that too, this time using my &lt;a href="https://github.com/simonw/files-to-prompt"&gt;files-to-prompt&lt;/a&gt; and &lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt; command-line tools:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;files-to-prompt &lt;span class="pl-c1"&gt;.&lt;/span&gt; --ignore LICENSE &lt;span class="pl-k"&gt;|&lt;/span&gt; \
  llm -m claude-3.5-sonnet -s \
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;step by step advice on how to implement automated tests for this, which is hard because the tests need to work within a temporary Django project that lives in the tests/ directory somehow. Provide all code at the end.&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/a1c51e3a4f30d91eac4664ba84266ca1#response"&gt;Claude's full response&lt;/a&gt;. It almost worked! It gave me a minimal test project in &lt;a href="https://github.com/simonw/django-http-debug/tree/1d2fae7141b1bdd9b156858e689511e282bd7b5a/tests/test_project"&gt;tests/test_project&lt;/a&gt; and an initial set of &lt;a href="https://github.com/simonw/django-http-debug/blob/1d2fae7141b1bdd9b156858e689511e282bd7b5a/tests/test_django_http_debug.py"&gt;quite sensible tests&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Sadly it didn’t quite solve the most fiddly problem for me: configuring it so running &lt;code&gt;pytest&lt;/code&gt; would correctly set the Python path and &lt;code&gt;DJANGO_SETTINGS_MODULE&lt;/code&gt; in order run the tests. I saw this error instead:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;django.core.exceptions.ImproperlyConfigured: Requested setting INSTALLED_APPS, but settings are not configured. You must either define the environment variable DJANGO_SETTINGS_MODULE or call settings.configure() before accessing settings.&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I spent some time with the &lt;a href="https://pytest-django.readthedocs.io/en/latest/managing_python_path.html"&gt;relevant pytest-django documentation&lt;/a&gt; and figure out a pattern that worked. Short version: I added this to my &lt;code&gt;pyproject.toml&lt;/code&gt; file:&lt;/p&gt;
&lt;div class="highlight highlight-source-toml"&gt;&lt;pre&gt;[&lt;span class="pl-en"&gt;tool&lt;/span&gt;.&lt;span class="pl-en"&gt;pytest&lt;/span&gt;.&lt;span class="pl-en"&gt;ini_options&lt;/span&gt;]
&lt;span class="pl-smi"&gt;DJANGO_SETTINGS_MODULE&lt;/span&gt; = &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;tests.test_project.settings&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-smi"&gt;pythonpath&lt;/span&gt; = [&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;.&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;]&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;For the longer version, take a look at my full TIL: &lt;a href="https://til.simonwillison.net/django/pytest-django"&gt;Using pytest-django with a reusable Django application&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="test-supported-cleanup"&gt;Test-supported cleanup&lt;/h4&gt;
&lt;p&gt;The great thing about having comprehensive tests in place is it makes iterating on the project much faster. Claude had used some patterns that weren’t necessary. I spent a few minutes seeing if the tests still passed if I deleted various pieces of code, and &lt;a href="https://github.com/simonw/django-http-debug/compare/1d2fae7141b1bdd9b156858e689511e282bd7b5a...97bab5dd9c7f4363a49127711c4c68ef1f3b6ade/"&gt;cleaned things up quite a bit&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="was-claude-worth-it-"&gt;Was Claude worth it?&lt;/h4&gt;
&lt;p&gt;This entire project took about two hours - just within a tolerable amount of time for what was effectively a useful &lt;a href="https://simonwillison.net/2024/Mar/22/claude-and-chatgpt-case-study/"&gt;sidequest&lt;/a&gt; from my intended activity for the day.&lt;/p&gt;
&lt;p&gt;Claude didn't implement the whole project for me. The code it produced didn't quite work - I had to tweak just a few lines of code, but knowing which code to tweak took a development environment and manual testing and benefited greatly from my 20+ years of Django experience!&lt;/p&gt;
&lt;p&gt;This is yet another example of how LLMs don't replace human developers: they augment us.&lt;/p&gt;
&lt;p&gt;The end result is a tool that I'm already using to solve real-world problems, and a &lt;a href="https://github.com/simonw/django-http-debug"&gt;code repository&lt;/a&gt; that I'm proud to put my name to. Without LLM assistance this project would have stayed on my ever-growing list of "things I'd love to build one day".&lt;/p&gt;
&lt;p&gt;I'm also really happy to have my own &lt;a href="https://til.simonwillison.net/django/pytest-django"&gt;documented solution&lt;/a&gt; to the challenge of adding automated tests to a standalone reusable Django application. I was tempted to skip this step entirely, but thanks to Claude's assistance I was able to break that problem open and come up with a solution that I'm really happy with.&lt;/p&gt;
&lt;p&gt;Last year I wrote about how &lt;a href="https://simonwillison.net/2023/Mar/27/ai-enhanced-development/"&gt;AI-enhanced development makes me more ambitious with my projects&lt;/a&gt;. It's also helping me be more diligent in not taking shortcuts like skipping setting up automated tests.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django-admin"&gt;django-admin&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webhooks"&gt;webhooks&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/anthropic"&gt;anthropic&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude"&gt;claude&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-3-5-sonnet"&gt;claude-3-5-sonnet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="django"/><category term="django-admin"/><category term="http"/><category term="projects"/><category term="python"/><category term="webhooks"/><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="anthropic"/><category term="claude"/><category term="claude-3-5-sonnet"/></entry><entry><title>django-http-debug 0.2</title><link href="https://simonwillison.net/2024/Aug/7/django-http-debug/#atom-tag" rel="alternate"/><published>2024-08-07T20:17:55+00:00</published><updated>2024-08-07T20:17:55+00:00</updated><id>https://simonwillison.net/2024/Aug/7/django-http-debug/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/django-http-debug/releases/tag/0.2"&gt;django-http-debug 0.2&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="http"/></entry><entry><title>django-http-debug 0.1a0</title><link href="https://simonwillison.net/2024/Aug/7/django-http-debug-2/#atom-tag" rel="alternate"/><published>2024-08-07T18:15:27+00:00</published><updated>2024-08-07T18:15:27+00:00</updated><id>https://simonwillison.net/2024/Aug/7/django-http-debug-2/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/django-http-debug/releases/tag/0.1a0"&gt;django-http-debug 0.1a0&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="http"/></entry><entry><title>How I write HTTP services in Go after 13 years</title><link href="https://simonwillison.net/2024/Feb/9/how-i-write-http-services-in-go-after-13-years/#atom-tag" rel="alternate"/><published>2024-02-09T20:40:23+00:00</published><updated>2024-02-09T20:40:23+00:00</updated><id>https://simonwillison.net/2024/Feb/9/how-i-write-http-services-in-go-after-13-years/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://grafana.com/blog/2024/02/09/how-i-write-http-services-in-go-after-13-years/"&gt;How I write HTTP services in Go after 13 years&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Useful set of current best practices for deploying HTTP servers written in Go. I guess Go counts as boring technology these days, which is high praise in my book.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=39318867"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/go"&gt;go&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;&lt;/p&gt;



</summary><category term="go"/><category term="http"/></entry><entry><title>Cloudflare does not consider vary values in caching decisions</title><link href="https://simonwillison.net/2023/Nov/20/cloudflare-does-not-consider-vary-values-in-caching-decisions/#atom-tag" rel="alternate"/><published>2023-11-20T05:08:52+00:00</published><updated>2023-11-20T05:08:52+00:00</updated><id>https://simonwillison.net/2023/Nov/20/cloudflare-does-not-consider-vary-values-in-caching-decisions/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developers.cloudflare.com/cache/concepts/cache-control/#other"&gt;Cloudflare does not consider vary values in caching decisions&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Here’s the spot in Cloudflare’s documentation where they hide a crucially important detail:&lt;/p&gt;

&lt;p&gt;“Cloudflare does not consider vary values in caching decisions. Nevertheless, vary values are respected when Vary for images is configured and when the vary header is vary: accept-encoding.”&lt;/p&gt;

&lt;p&gt;This means you can’t deploy an application that uses content negotiation via the Accept header behind the Cloudflare CDN—for example serving JSON or HTML for the same URL depending on the incoming Accept header. If you do, Cloudflare may serve cached JSON to an HTML client or vice-versa.&lt;/p&gt;

&lt;p&gt;There’s an exception for image files, which Cloudflare added support for in September 2021 (for Pro accounts only) in order to support formats such as WebP which may not have full support across all browsers.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cloudflare"&gt;cloudflare&lt;/a&gt;&lt;/p&gt;



</summary><category term="caching"/><category term="http"/><category term="cloudflare"/></entry><entry><title>See this page fetch itself, byte by byte, over TLS</title><link href="https://simonwillison.net/2023/May/10/see-this-page-fetch-itself-byte-by-byte-over-tls/#atom-tag" rel="alternate"/><published>2023-05-10T13:58:36+00:00</published><updated>2023-05-10T13:58:36+00:00</updated><id>https://simonwillison.net/2023/May/10/see-this-page-fetch-itself-byte-by-byte-over-tls/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://subtls.pages.dev/"&gt;See this page fetch itself, byte by byte, over TLS&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
George MacKerron built a TLS 1.3 library in TypeScript and used it to construct this amazing educational demo, which performs a full HTTPS request for its own source code over a WebSocket and displays an annotated byte-by-byte representation of the entire exchange. This is the most useful illustration of how HTTPS actually works that I’ve ever seen.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/b0rk/status/1656287855612682240"&gt;Julia Evans&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/encryption"&gt;encryption&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/https"&gt;https&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tls"&gt;tls&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/websockets"&gt;websockets&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/explorables"&gt;explorables&lt;/a&gt;&lt;/p&gt;



</summary><category term="encryption"/><category term="http"/><category term="https"/><category term="tls"/><category term="websockets"/><category term="explorables"/></entry><entry><title>urllib3 v2.0.0 is now generally available</title><link href="https://simonwillison.net/2023/Apr/26/urllib3/#atom-tag" rel="alternate"/><published>2023-04-26T22:00:16+00:00</published><updated>2023-04-26T22:00:16+00:00</updated><id>https://simonwillison.net/2023/Apr/26/urllib3/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://sethmlarson.dev/urllib3-2.0.0"&gt;urllib3 v2.0.0 is now generally available&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
urllib3 is 12 years old now, and is a common low-level dependency for packages like requests and httpx. The biggest new feature in v2 is a higher-level API: resp = urllib3.request(“GET”, “https://example.com”)—a very welcome addition to the library.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;&lt;/p&gt;



</summary><category term="http"/><category term="python"/></entry><entry><title>RFC 7807: Problem Details for HTTP APIs</title><link href="https://simonwillison.net/2022/Nov/1/rfc-7807/#atom-tag" rel="alternate"/><published>2022-11-01T03:15:05+00:00</published><updated>2022-11-01T03:15:05+00:00</updated><id>https://simonwillison.net/2022/Nov/1/rfc-7807/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://datatracker.ietf.org/doc/draft-ietf-httpapi-rfc7807bis/"&gt;RFC 7807: Problem Details for HTTP APIs&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
This RFC has been brewing for quite a while, and is currently in last call (ends 2022-11-03). I’m designing the JSON error messages for Datasette at the moment so this could not be more relevant for me.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://blog.frankel.ch/structured-errors-http-apis/"&gt;Nicolas Fränkel&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


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



</summary><category term="errors"/><category term="http"/><category term="json"/><category term="mark-nottingham"/><category term="rfc"/><category term="standards"/></entry><entry><title>Introducing sqlite-http: A SQLite extension for making HTTP requests</title><link href="https://simonwillison.net/2022/Aug/10/sqlite-http/#atom-tag" rel="alternate"/><published>2022-08-10T22:22:42+00:00</published><updated>2022-08-10T22:22:42+00:00</updated><id>https://simonwillison.net/2022/Aug/10/sqlite-http/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://observablehq.com/@asg017/introducing-sqlite-http"&gt;Introducing sqlite-http: A SQLite extension for making HTTP requests&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Characteristically thoughtful SQLite extension from Alex, following his sqlite-html extension from a few days ago. sqlite-http lets you make HTTP requests from SQLite—both as a SQL function that returns a string, and as a table-valued SQL function that lets you independently access the body, headers and even the timing data for the request.&lt;/p&gt;

&lt;p&gt;This write-up is excellent: it provides interactive demos but also shows how additional SQLite extensions such as the new-to-me “define” extension can be combined with sqlite-http to create custom functions for parsing and processing HTML.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/agarcia_me/status/1557437368818249728"&gt;@agarcia_me&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/alex-garcia"&gt;alex-garcia&lt;/a&gt;&lt;/p&gt;



</summary><category term="http"/><category term="sqlite"/><category term="alex-garcia"/></entry><entry><title>Making HTTP calls using IPv6</title><link href="https://simonwillison.net/2022/Jun/8/http-ipv6/#atom-tag" rel="alternate"/><published>2022-06-08T22:58:28+00:00</published><updated>2022-06-08T22:58:28+00:00</updated><id>https://simonwillison.net/2022/Jun/8/http-ipv6/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;TIL:&lt;/strong&gt; &lt;a href="https://til.simonwillison.net/networking/http-ipv6"&gt;Making HTTP calls using IPv6&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="http"/></entry><entry><title>curlconverter.com</title><link href="https://simonwillison.net/2022/Mar/10/curlconvertercom/#atom-tag" rel="alternate"/><published>2022-03-10T20:12:44+00:00</published><updated>2022-03-10T20:12:44+00:00</updated><id>https://simonwillison.net/2022/Mar/10/curlconvertercom/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://curlconverter.com/"&gt;curlconverter.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
This is pretty magic: paste in a “curl” command (including the ones you get from browser devtools using copy-as-curl) and this will convert that into code for making the same HTTP request... using Python, JavaScript, PHP, R, Go, Rust, Elixir, Java, MATLAB, Ansible URI, Strest, Dart or JSON.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://jvns.ca/blog/2022/03/10/how-to-use-undocumented-web-apis/"&gt;Julia Evans&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/curl"&gt;curl&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;&lt;/p&gt;



</summary><category term="curl"/><category term="http"/></entry><entry><title>Hurl</title><link href="https://simonwillison.net/2021/Nov/22/hurl/#atom-tag" rel="alternate"/><published>2021-11-22T03:32:33+00:00</published><updated>2021-11-22T03:32:33+00:00</updated><id>https://simonwillison.net/2021/Nov/22/hurl/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Orange-OpenSource/hurl"&gt;Hurl&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Hurl is “a command line tool that runs HTTP requests defined in a simple plain text format”—written in Rust on top of curl, it lets you run HTTP requests and then execute assertions against the response, defined using JSONPath or XPath for HTML. It can even assert that responses were returned within a specified duration.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/humphd/status/1462594205629493254"&gt;@humphd&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/curl"&gt;curl&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rust"&gt;rust&lt;/a&gt;&lt;/p&gt;



</summary><category term="curl"/><category term="http"/><category term="rust"/></entry><entry><title>New HTTP standards for caching on the modern web</title><link href="https://simonwillison.net/2021/Oct/21/new-http-standards-for-caching-on-the-modern-web/#atom-tag" rel="alternate"/><published>2021-10-21T22:40:50+00:00</published><updated>2021-10-21T22:40:50+00:00</updated><id>https://simonwillison.net/2021/Oct/21/new-http-standards-for-caching-on-the-modern-web/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://httptoolkit.tech/blog/status-targeted-caching-headers/`"&gt;New HTTP standards for caching on the modern web&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Cache-Status is a new HTTP header (RFC from August 2021) designed to provide better debugging information about which caches were involved in serving a request—“Cache-Status: Nginx; hit, Cloudflare; fwd=stale; fwd-status=304; collapsed; ttl=300” for example indicates that Nginx served a cache hit, then Cloudflare had a stale cached version so it revalidated from Nginx, got a 304 not modified, collapsed multiple requests (dogpile prevention) and plans to serve the new cached value for the next five minutes. Also described is $Target-Cache-Control: which allows different CDNs to respond to different headers and is already supported by Cloudflare and Akamai (Cloudflare-CDN-Cache-Control: and Akamai-Cache-Control:).

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=28930941"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/dogpile"&gt;dogpile&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cloudflare"&gt;cloudflare&lt;/a&gt;&lt;/p&gt;



</summary><category term="caching"/><category term="dogpile"/><category term="http"/><category term="cloudflare"/></entry><entry><title>Notes on streaming large API responses</title><link href="https://simonwillison.net/2021/Jun/25/streaming-large-api-responses/#atom-tag" rel="alternate"/><published>2021-06-25T16:26:49+00:00</published><updated>2021-06-25T16:26:49+00:00</updated><id>https://simonwillison.net/2021/Jun/25/streaming-large-api-responses/#atom-tag</id><summary type="html">
    &lt;p&gt;I started &lt;a href="https://twitter.com/simonw/status/1405554676993433605"&gt;a Twitter conversation&lt;/a&gt; last week about API endpoints that stream large amounts of data as an alternative to APIs that return 100 results at a time and require clients to paginate through all of the pages in order to retrieve all of the data:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p dir="ltr" lang="en"&gt;Any unexpected downsides to offering streaming HTTP API endpoints that serve up eg 100,000 JSON objects in a go rather than asking users to paginate 100 at a time over 1,000 requests, assuming efficient implementation of that streaming endpoint?&lt;/p&gt;— Simon Willison (@simonw) &lt;a href="https://twitter.com/simonw/status/1405554676993433605?ref_src=twsrc%5Etfw"&gt;June 17, 2021&lt;/a&gt;
&lt;/blockquote&gt;
&lt;p&gt;I got a ton of great replies. I tried to tie them together in a thread attached to the tweet, but I'm also going to synthesize them into some thoughts here.&lt;/p&gt;
&lt;h4&gt;Bulk exporting data&lt;/h4&gt;
&lt;p&gt;The more time I spend with APIs, especially with regard to my &lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt; and &lt;a href="https://simonwillison.net/2020/Nov/14/personal-data-warehouses/"&gt;Dogsheep&lt;/a&gt; projects, the more I realize that my favourite APIs are the ones that let you extract &lt;em&gt;all&lt;/em&gt; of your data as quickly and easily as possible.&lt;/p&gt;
&lt;p&gt;There are generally three ways an API might provide this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Click an "export everything" button, then wait for a while for an email to show up with a link to a downloadable zip file. This isn't really an API, in particular since it's usually hard if not impossible to automate that initial "click", but it's still better than nothing. Google's &lt;a href="https://takeout.google.com/"&gt;Takeout&lt;/a&gt; is one notable implementation of this pattern.&lt;/li&gt;
&lt;li&gt;Provide a JSON API which allows users to paginate through their data. This is a very common pattern, although it can run into difficulties: what happens if new data is added while you are paginating through the original data, for example? Some systems only allow access to the first N pages too, for performance reasons.&lt;/li&gt;
&lt;li&gt;Providing a single HTTP endpoint you can hit that will return ALL of your data - potentially dozens or hundreds of MBs of it - in one go.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It's that last option that I'm interested in talking about today.&lt;/p&gt;
&lt;h4&gt;Efficiently streaming data&lt;/h4&gt;
&lt;p&gt;It used to be that most web engineers would quickly discount the idea of an API endpoint that streams out an unlimited number of rows. HTTP requests should be served as quickly as possible! Anything more than a couple of seconds spent processing a request is a red flag that something should be reconsidered.&lt;/p&gt;
&lt;p&gt;Almost everything in the web stack is optimized for quickly serving small requests. But over the past decade the tide has turned somewhat: Node.js made async web servers commonplace, WebSockets taught us to handle long-running connections and in the Python world asyncio and &lt;a href="https://asgi.readthedocs.io/"&gt;ASGI&lt;/a&gt; provided a firm foundation for handling long-running requests using smaller amounts of RAM and CPU.&lt;/p&gt;
&lt;p&gt;I've been experimenting in this area for a few years now.&lt;/p&gt;
&lt;p&gt;Datasette has the ability to &lt;a href="https://github.com/simonw/datasette/blob/0.57.1/datasette/views/base.py#L264-L428"&gt;use ASGI trickery&lt;/a&gt; to &lt;a href="https://docs.datasette.io/en/stable/csv_export.html#streaming-all-records"&gt;stream all rows from a table&lt;/a&gt; (or filtered table) as CSV, potentially returning hundreds of MBs of data.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://django-sql-dashboard.datasette.io/"&gt;Django SQL Dashboard&lt;/a&gt; can export the full results of a SQL query as CSV or TSV, this time using Django's &lt;a href="https://docs.djangoproject.com/en/3.2/ref/request-response/#django.http.StreamingHttpResponse"&gt;StreamingHttpResponse&lt;/a&gt; (which does tie up a full worker process, but that's OK if you restrict it to a controlled number of authenticated users).&lt;/p&gt;
&lt;p&gt;&lt;a href="https://simonwillison.net/tags/vaccinateca/"&gt;VIAL&lt;/a&gt; implements streaming responses to offer an &lt;a href="https://github.com/CAVaccineInventory/vial/blob/cdaaab053a9cf1cef40104a2cdf480b7932d58f7/vaccinate/core/admin_actions.py"&gt;"export from the admin" feature&lt;/a&gt;. It also has an API-key-protected search API which can stream out all matching rows &lt;a href="https://github.com/CAVaccineInventory/vial/blob/cdaaab053a9cf1cef40104a2cdf480b7932d58f7/vaccinate/api/serialize.py#L38"&gt;in JSON or GeoJSON&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;Implementation notes&lt;/h4&gt;
&lt;p&gt;The key thing to watch out for when implementing this pattern is memory usage: if your server buffers 100MB+ of data any time it needs to serve an export request you're going to run into trouble.&lt;/p&gt;
&lt;p&gt;Some export formats are friendlier for streaming than others. CSV and TSV are pretty easy to stream, as is newline-delimited JSON.&lt;/p&gt;
&lt;p&gt;Regular JSON requires a bit more thought: you can output a &lt;code&gt;[&lt;/code&gt; character, then output each row in a stream with a comma suffix, then skip the comma for the last row and output a &lt;code&gt;]&lt;/code&gt;. Doing that requires peeking ahead (looping two at a time) to verify that you haven't yet reached the end.&lt;/p&gt;
&lt;p&gt;Or... Martin De Wulf &lt;a href="https://twitter.com/madewulf/status/1405559088994467844"&gt;pointed out&lt;/a&gt; that you can output the first row, then output every other row with a preceeding comma - which avoids the whole "iterate two at a time" problem entirely.&lt;/p&gt;
&lt;p&gt;The next challenge is efficiently looping through every database result without first pulling them all into memory.&lt;/p&gt;
&lt;p&gt;PostgreSQL (and the &lt;code&gt;psycopg2&lt;/code&gt; Python module) offers &lt;a href="https://www.psycopg.org/docs/usage.html#server-side-cursors"&gt;server-side cursors&lt;/a&gt;, which means you can stream results through your code without loading them all at once. I use these &lt;a href="https://github.com/simonw/django-sql-dashboard/blob/dd1bb18e45b40ce8f3d0553a72b7ec3cdc329e69/django_sql_dashboard/views.py#L397-L399"&gt;in Django SQL Dashboard&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Server-side cursors make me nervous though, because they seem like they likely tie up resources in the database itself. So the other technique I would consider here is &lt;a href="https://use-the-index-luke.com/no-offset"&gt;keyset pagination&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Keyset pagination works against any data that is ordered by a unique column - it works especially well against a primary key (or other indexed column). Each page of data is retrieved using a query something like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-sql"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;select&lt;/span&gt; &lt;span class="pl-k"&gt;*&lt;/span&gt; &lt;span class="pl-k"&gt;from&lt;/span&gt; items &lt;span class="pl-k"&gt;order by&lt;/span&gt; id &lt;span class="pl-k"&gt;limit&lt;/span&gt; &lt;span class="pl-c1"&gt;21&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Note the &lt;code&gt;limit 21&lt;/code&gt; - if we are retrieving pages of 20 items we ask for 21, since then we can use the last returned item to tell if there is a next page or not.&lt;/p&gt;
&lt;p&gt;Then for subsequent pages take the 20th &lt;code&gt;id&lt;/code&gt; value and ask for things greater than that:&lt;/p&gt;
&lt;div class="highlight highlight-source-sql"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;select&lt;/span&gt; &lt;span class="pl-k"&gt;*&lt;/span&gt; &lt;span class="pl-k"&gt;from&lt;/span&gt; items &lt;span class="pl-k"&gt;where&lt;/span&gt; id &lt;span class="pl-k"&gt;&amp;gt;&lt;/span&gt; &lt;span class="pl-c1"&gt;20&lt;/span&gt; &lt;span class="pl-k"&gt;limit&lt;/span&gt; &lt;span class="pl-c1"&gt;21&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Each of these queries is fast to respond (since it's against an ordered index) and uses a predictable, fixed amount of memory. Using keyset pagination we can loop through an abitrarily large table of data, streaming each page out one at a time, without exhausting any resources.&lt;/p&gt;
&lt;p&gt;And since each query is small and fast, we don't need to worry about huge queries tying up database resources either.&lt;/p&gt;
&lt;h4&gt;What can go wrong?&lt;/h4&gt;
&lt;p&gt;I really like these patterns. They haven't bitten me yet, though I've not deployed them for anything truly huge scale. So I &lt;a href="https://twitter.com/simonw/status/1405554676993433605"&gt;asked Twitter&lt;/a&gt; what kind of problems I should look for.&lt;/p&gt;
&lt;p&gt;Based on the Twitter conversation, here are some of the challenges that this approach faces.&lt;/p&gt;
&lt;h4&gt;Challenge: restarting servers&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p dir="ltr" lang="en"&gt;If the stream takes a significantly long time to finish then rolling out updates becomes a problem. You don't want to interrupt a download but also don't want to wait forever for it to finish to spin down the server.&lt;/p&gt;— Adam Lowry (@robotadam) &lt;a href="https://twitter.com/robotadam/status/1405556544897384459?ref_src=twsrc%5Etfw"&gt;June 17, 2021&lt;/a&gt;
&lt;/blockquote&gt;
&lt;p&gt;This came up a few times, and is something I hadn't considered. If your deployment process involves restarting your servers (and it's hard to imagine one that doesn't) you need to take long-running connections into account when you do that. If there's a user half way through a 500MB stream you can either truncate their connection or wait for them to finish.&lt;/p&gt;
&lt;h4 id="challenge-errors"&gt;Challenge: how to return errors&lt;/h4&gt;
&lt;p&gt;If you're streaming a response, you start with an HTTP 200 code... but then what happens if an error occurs half-way through, potentially while paginating through the database?&lt;/p&gt;
&lt;p&gt;You've already started sending the request, so you can't change the status code to a 500. Instead, you need to write some kind of error to the stream that's being produced.&lt;/p&gt;
&lt;p&gt;If you're serving up a huge JSON document, you can at least make that JSON become invalid, which should indicate to your client that something went wrong.&lt;/p&gt;
&lt;p&gt;Formats like CSV are harder. How do you let your user know that their CSV data is incomplete?&lt;/p&gt;
&lt;p&gt;And what if someone's connection drops - are they definitely going to notice that they are missing something, or will they assume that the truncated file is all of the data?&lt;/p&gt;
&lt;h4&gt;Challenge: resumable downloads&lt;/h4&gt;
&lt;p&gt;If a user is paginating through your API, they get resumability for free: if something goes wrong they can start again at the last page that they fetched.&lt;/p&gt;
&lt;p&gt;Resuming a single stream is a lot harder.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests"&gt;HTTP range mechanism&lt;/a&gt; can be used to provide resumable downloads against large files, but it only works if you generate the entire file in advance.&lt;/p&gt;
&lt;p&gt;There is a way to design APIs to support this, provided the data in the stream is in a predictable order (which it has to be if you're using keyset pagination, described above).&lt;/p&gt;
&lt;p&gt;Have the endpoint that triggers the download take an optional &lt;code&gt;?since=&lt;/code&gt; parameter, like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /stream-everything?since=b24ou34
[
    {"id": "m442ecc", "name": "..."},
    {"id": "c663qo2", "name": "..."},
    {"id": "z434hh3", "name": "..."},
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here the &lt;code&gt;b24ou34&lt;/code&gt; is an identifier - it can be a deliberately opaque token, but it needs to be served up as part of the response.&lt;/p&gt;
&lt;p&gt;If the user is disconnected for any reason, they can start back where they left off by passing in the last ID that they successfully retrieved:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /stream-everything?since=z434hh3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This still requires some level of intelligence from the client application, but it's a reasonably simple pattern both to implement on the server and as a client.&lt;/p&gt;
&lt;h4&gt;Easiest solution: generate and return from cloud storage&lt;/h4&gt;
&lt;p&gt;It seems the most robust way to implement this kind of API is the least technically exciting: spin off a background task that generates the large response and pushes it to cloud storage (S3 or GCS), then redirect the user to a signed URL to download the resulting file.&lt;/p&gt;
&lt;p&gt;This is easy to scale, gives users complete files with content-length headers that they know they can download (and even resume-downloading, since range headers are supported by S3 and GCS). It also avoids any issues with server restarts caused by long connections.&lt;/p&gt;
&lt;p&gt;This is how Mixpanel handle their export feature, and it's &lt;a href="https://seancoates.com/blogs/lambda-payload-size-workaround"&gt;the solution Sean Coates came to&lt;/a&gt; when trying to find a workaround for the AWS Lambda/API Gate response size limit.&lt;/p&gt;
&lt;p&gt;If your goal is to provide your users a robust, reliable bulk-export mechanism for their data, export to cloud storage is probably the way to go.&lt;/p&gt;
&lt;p&gt;But streaming dynamic responses are a really neat trick, and I plan to keep exploring them!&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/apis"&gt;apis&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/scaling"&gt;scaling&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/streaming"&gt;streaming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/asgi"&gt;asgi&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http-range-requests"&gt;http-range-requests&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="apis"/><category term="http"/><category term="scaling"/><category term="streaming"/><category term="asgi"/><category term="http-range-requests"/></entry><entry><title>Weeknotes: Archiving coronavirus.data.gov.uk, custom pages and directory configuration in Datasette, photos-to-sqlite</title><link href="https://simonwillison.net/2020/Apr/29/weeknotes/#atom-tag" rel="alternate"/><published>2020-04-29T19:41:11+00:00</published><updated>2020-04-29T19:41:11+00:00</updated><id>https://simonwillison.net/2020/Apr/29/weeknotes/#atom-tag</id><summary type="html">
    &lt;p&gt;I mainly made progress on three projects this week: Datasette, photos-to-sqlite and a cleaner way of archiving data to a git repository.&lt;/p&gt;

&lt;h3&gt;Archiving coronavirus.data.gov.uk&lt;/h3&gt;

&lt;p&gt;The UK goverment have a new portal website sharing detailed Coronavirus data for regions around the country, at &lt;a href="https://coronavirus.data.gov.uk/"&gt;coronavirus.data.gov.uk&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;As with everything else built in 2020, it's a big single-page JavaScript app. Matthew Somerville &lt;a href="http://dracos.co.uk/wrote/coronavirus-dashboard/"&gt;investigated&lt;/a&gt; what it would take to build a much lighter (and faster loading) site displaying the same information by moving much of the rendering to the server.&lt;/p&gt;

&lt;p&gt;One of the best things about the SPA craze is that it strongly encourages structured data to be published as JSON files. Matthew's article inspired me to take a look, and sure enough the government figures are available in an extremely comprehensive (and 3.3MB in size) JSON file, available from &lt;a href="https://c19downloads.azureedge.net/downloads/data/data_latest.json"&gt;https://c19downloads.azureedge.net/downloads/data/data_latest.json&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Any time I see a file like this my first questions are how often does it change - and what kind of changes are being made to it?&lt;/p&gt;

&lt;p&gt;I've written about scraping to a git repository (see my new &lt;a href="https://simonwillison.net/tags/gitscraping/"&gt;gitscraping&lt;/a&gt; tag) a bunch in the past:&lt;/p&gt;

&lt;ul&gt;&lt;li&gt;&lt;a href="https://simonwillison.net/2017/Sep/10/scraping-irma/"&gt;Scraping hurricane Irma&lt;/a&gt; - September 2017&lt;/li&gt;&lt;li&gt;&lt;a href="https://simonwillison.net/2017/Oct/10/fires-in-the-north-bay/"&gt;Changelogs to help understand the fires in the North Bay&lt;/a&gt; - October 2017&lt;/li&gt;&lt;li&gt;&lt;a href="https://simonwillison.net/2019/Mar/13/tree-history/"&gt;Generating a commit log for San Francisco’s official list of trees&lt;/a&gt; - March 2019&lt;/li&gt;&lt;li&gt;&lt;a href="https://simonwillison.net/2019/Oct/10/pge-outages/"&gt;Tracking PG&amp;amp;E outages by scraping to a git repo&lt;/a&gt; - October 2019&lt;/li&gt;&lt;li&gt;&lt;a href="https://simonwillison.net/2020/Jan/21/github-actions-cloud-run/"&gt;Deploying a data API using GitHub Actions and Cloud Run&lt;/a&gt; - January 2020&lt;/li&gt;&lt;/ul&gt;

&lt;p&gt;Now that I've figured out a really clean way to &lt;a href="https://github.com/simonw/til/blob/master/github-actions/commit-if-file-changed.md"&gt;Commit a file if it changed&lt;/a&gt; in a GitHub Action knocking out new versions of this pattern is really quick.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/simonw/coronavirus-data-gov-archive"&gt;simonw/coronavirus-data-gov-archive&lt;/a&gt; is my new repo that does exactly that: it periodically fetches the latest versions of the JSON data files powering that site and commits them if they have changed. The aim is to build a &lt;a href="https://github.com/simonw/coronavirus-data-gov-archive/commits/master/data_latest.json"&gt;commit history&lt;/a&gt; of changes made to the underlying data.&lt;/p&gt;

&lt;p&gt;The first implementation was extremely simple - here's the &lt;a href="https://github.com/simonw/coronavirus-data-gov-archive/blob/c83d69e95ec6400bf77d7b0d474e868baa78841e/.github/workflows/scheduled.yml"&gt;entire action&lt;/a&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;name: Fetch latest data

on:
push:
repository_dispatch:
schedule:
    - cron:  '25 * * * *'

jobs:
scheduled:
    runs-on: ubuntu-latest
    steps:
    - name: Check out this repo
    uses: actions/checkout@v2
    - name: Fetch latest data
    run: |-
        curl https://c19downloads.azureedge.net/downloads/data/data_latest.json | jq . &amp;gt; data_latest.json
        curl https://c19pub.azureedge.net/utlas.geojson | gunzip | jq . &amp;gt; utlas.geojson
        curl https://c19pub.azureedge.net/countries.geojson | gunzip | jq . &amp;gt; countries.geojson
        curl https://c19pub.azureedge.net/regions.geojson | gunzip | jq . &amp;gt; regions.geojson
    - name: Commit and push if it changed
    run: |-
        git config user.name "Automated"
        git config user.email "actions@users.noreply.github.com"
        git add -A
        timestamp=$(date -u)
        git commit -m "Latest data: ${timestamp}" || exit 0
        git push&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It uses a combination of &lt;code&gt;curl&lt;/code&gt; and &lt;code&gt;jq&lt;/code&gt; (both available &lt;a href="https://github.com/actions/virtual-environments/blob/master/images/linux/Ubuntu1804-README.md"&gt;in the default worker environment&lt;/a&gt;) to pull down the data and pretty-print it (better for readable diffs), then commits the result.&lt;/p&gt;

&lt;p&gt;Matthew Somerville &lt;a href="https://twitter.com/dracos/status/1255221799085846532"&gt;pointed out&lt;/a&gt; that inefficient polling sets a bad precedent. Here I'm hitting &lt;code&gt;azureedge.net&lt;/code&gt;, the Azure CDN, so that didn't particularly worry me - but since I want this pattern to be used widely it's good to provide a best-practice example.&lt;/p&gt;

&lt;p&gt;Figuring out the best way to make &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests"&gt;conditional get requests&lt;/a&gt; in a GitHub Action lead me down &lt;a href="https://github.com/simonw/coronavirus-data-gov-archive/issues/1"&gt;something of a rabbit hole&lt;/a&gt;. I wanted to use &lt;a href="https://daniel.haxx.se/blog/2019/12/06/curl-speaks-etag/"&gt;curl's new ETag support&lt;/a&gt; but I ran into &lt;a href="https://github.com/curl/curl/issues/5309"&gt;a curl bug&lt;/a&gt;, so I ended up rolling a simple Python CLI tool called &lt;a href="https://github.com/simonw/conditional-get"&gt;conditional-get&lt;/a&gt; to solve my problem. In the time it took me to release that tool (just a few hours) a &lt;a href="https://github.com/curl/curl/issues/5309#issuecomment-621265179"&gt;new curl release&lt;/a&gt; came out with a fix for that bug!&lt;/p&gt;

&lt;p&gt;Here's &lt;a href="https://github.com/simonw/coronavirus-data-gov-archive/blob/a95d7661b236a9ee9a26a441dd948eb00308f919/.github/workflows/scheduled.yml"&gt;the workflow&lt;/a&gt; using my &lt;code&gt;conditional-get&lt;/code&gt; tool. See &lt;a href="https://github.com/simonw/coronavirus-data-gov-archive/issues/1"&gt;the issue thread&lt;/a&gt; for all of the other potential solutions, including a really neat &lt;a href="https://github.com/hubgit/curl-etag"&gt;Action shell-script solution&lt;/a&gt; by Alf Eaton.&lt;/p&gt;

&lt;p&gt;To my absolute delight, the project has already been forked once by Daniel Langer to &lt;a href="https://github.com/dlanger/coronavirus-hc-infobase-archive"&gt;capture Canadian Covid-19 cases&lt;/a&gt;!&lt;/p&gt;

&lt;h3 id="new-datasette-features"&gt;New Datasette features&lt;/h3&gt;

&lt;p&gt;I pushed two new features to &lt;a href="https://github.com/simonw/datasette"&gt;Datasette&lt;/a&gt; master, ready for release in 0.41.&lt;/p&gt;

&lt;h4&gt;Configuration directory mode&lt;/h4&gt;

&lt;p&gt;This is an idea I had while building &lt;a href="https://github.com/simonw/datasette-publish-now"&gt;datasette-publish-now&lt;/a&gt;. Datasette instances can be run with custom metadata, custom plugins and custom templates. I'm increasingly finding myself working on projects that run using something like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ datasette data1.db data2.db data3.db \
    --metadata=metadata.json
    --template-dir=templates \
    --plugins-dir=plugins&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Directory configuration mode introduces the idea that Datasette can configure itself based on a directory layout. The above example can instead by handled by creating the following layout:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;my-project/data1.db
my-project/data2.db
my-project/data3.db
my-project/metadatata.json
my-project/templates/index.html
my-project/plugins/custom_plugin.py&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then run Datasette directly targetting that directory:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ datasette my-project/&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;See &lt;a href="https://github.com/simonw/datasette/issues/731"&gt;issue #731&lt;/a&gt; for more details. Directory configuration mode &lt;a href="https://datasette.readthedocs.io/en/latest/config.html#configuration-directory-mode"&gt;is documented here&lt;/a&gt;.&lt;/p&gt;

&lt;h4&gt;Define custom pages using templates/pages&lt;/h4&gt;

&lt;p&gt;In &lt;a href="https://simonwillison.net/2019/Nov/25/niche-museums/"&gt;niche-museums.com, powered by Datasette&lt;/a&gt; I described how I built the &lt;a href="https://www.niche-museums.com/"&gt;www.niche-museums.com&lt;/a&gt; website as a heavily customized Datasette instance.&lt;/p&gt;

&lt;p&gt;That site has &lt;a href="https://www.niche-museums.com/about"&gt;/about&lt;/a&gt; and &lt;a href="https://www.niche-museums.com/map"&gt;/map&lt;/a&gt; pages which are served by custom templates - but I had to do some gnarly hacks with empty &lt;code&gt;about.db&lt;/code&gt; and &lt;code&gt;map.db&lt;/code&gt; files to get them to work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/simonw/datasette/issues/648"&gt;Issue #648&lt;/a&gt; introduces a new mechanism for creating this kind of page: create a &lt;code&gt;templates/pages/map.html&lt;/code&gt; template file and custom 404 handling code will ensure that any hits to &lt;code&gt;/map&lt;/code&gt; serve the rendered contents of that template.&lt;/p&gt;

&lt;p&gt;This could work really well with the &lt;a href="https://github.com/simonw/datasette-template-sql"&gt;datasette-template-sql&lt;/a&gt; plugin, which allows templates to execute abritrary SQL queries (ala PHP or ColdFusion).&lt;/p&gt;

&lt;p&gt;Here's the new &lt;a href="https://datasette.readthedocs.io/en/latest/custom_templates.html#custom-pages"&gt;documentation on custom pages&lt;/a&gt;, including details of how to use the new &lt;code&gt;custom_status()&lt;/code&gt;, &lt;code&gt;custom_header()&lt;/code&gt; and &lt;code&gt;custom_redirect()&lt;/code&gt; template functions to go beyond just returning HTML.&lt;/p&gt;

&lt;h3&gt;photos-to-sqlite&lt;/h3&gt;

&lt;p&gt;My &lt;a href="https://dogsheep.github.io/"&gt;Dogsheep&lt;/a&gt; personal analytics project brings my &lt;a href="https://github.com/dogsheep/twitter-to-sqlite"&gt;tweets&lt;/a&gt;, &lt;a href="https://github.com/dogsheep/github-to-sqlite"&gt;GitHub activity&lt;/a&gt;, &lt;a href="https://github.com/dogsheep/swarm-to-sqlite"&gt;Swarm checkins&lt;/a&gt; and more together in one place. But the big missing feature is my photos.&lt;/p&gt;

&lt;p&gt;As-of yesterday, I have 39,000 photos from Apple Photos uploaded to an S3 bucket using my new &lt;a href="https://github.com/dogsheep/photos-to-sqlite/"&gt;photos-to-sqlite&lt;/a&gt; tool. I can run the following SQL query and get back ten random photos!&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;select
  json_object(
    'img_src',
    'https://photos.simonwillison.net/i/' || 
    sha256 || '.' || ext || '?w=400'
  ),
  filepath,
  ext
from
  photos
where
  ext in ('jpeg', 'jpg', 'heic')
order by
  random()
limit
  10&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;photos.simonwillison.net&lt;/code&gt; is running a modified version of my &lt;a href="https://github.com/simonw/heic-to-jpeg"&gt;heic-to-jpeg&lt;/a&gt; image converting and resizing proxy, which I'll release at some point soon.&lt;/p&gt;

&lt;p&gt;There's still plenty of work to do - I still need to import EXIF data (including locations) into SQLite, and I plan to use &lt;a href="https://github.com/RhetTbull/osxphotos"&gt;osxphotos&lt;/a&gt; to export additional metadata from my Apple Photos library. But this week it went from a pure research project to something I can actually start using, which is exciting.&lt;/p&gt;

&lt;h3&gt;TIL this week&lt;/h3&gt;

&lt;ul&gt;&lt;li&gt;&lt;a href="https://github.com/simonw/til/blob/master/macos/fixing-compinit-insecure-directories.md"&gt;Fixing "compinit: insecure directories" error&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="https://github.com/simonw/til/blob/master/tailscale/lock-down-sshd.md"&gt;Restricting SSH connections to devices within a Tailscale network&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="https://github.com/simonw/til/blob/master/python/generate-nested-json-summary.md"&gt;Generated a summary of nested JSON data&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="https://github.com/simonw/til/blob/master/pytest/session-scoped-tmp.md"&gt;Session-scoped temporary directories in pytest&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="https://github.com/simonw/til/blob/master/pytest/mock-httpx.md"&gt;How to mock httpx using pytest-mock&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;

&lt;p&gt;Generated using &lt;a href="https://til.simonwillison.net/til?sql=select+json_object(%27pre%27%2C+group_concat(%27*+[%27+||+title+||+%27](%27+||+url+||+%27)%27%2C+%27%0D%0A%27))+from+til+where+%22created_utc%22+%3E%3D+%3Ap0+order+by+updated_utc+desc+limit+101&amp;amp;p0=2020-04-23"&gt;this query&lt;/a&gt;.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/git"&gt;git&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/matthew-somerville"&gt;matthew-somerville&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/photos"&gt;photos&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/covid19"&gt;covid19&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/git-scraping"&gt;git-scraping&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="git"/><category term="http"/><category term="matthew-somerville"/><category term="photos"/><category term="projects"/><category term="datasette"/><category term="weeknotes"/><category term="covid19"/><category term="git-scraping"/></entry><entry><title>Async Support - HTTPX</title><link href="https://simonwillison.net/2020/Jan/10/httpx/#atom-tag" rel="alternate"/><published>2020-01-10T04:49:59+00:00</published><updated>2020-01-10T04:49:59+00:00</updated><id>https://simonwillison.net/2020/Jan/10/httpx/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.python-httpx.org/async/"&gt;Async Support - HTTPX&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
HTTPX is the new async-friendly HTTP library for Python spearheaded by Tom Christie. It works in both async and non-async mode with an API very similar to requests. The async support is particularly interesting - it's a really clean API, and now that Jupyter supports top-level await you can run &lt;code&gt;(await httpx.AsyncClient().get(url)).text&lt;/code&gt; directly in a cell and get back the response. Most excitingly the library lets you pass an ASGI app directly to the client and then perform requests against it - ideal for unit tests.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/_tomchristie/status/1215240517962870784"&gt;@_tomchristie&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/asgi"&gt;asgi&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/kim-christie"&gt;kim-christie&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/httpx"&gt;httpx&lt;/a&gt;&lt;/p&gt;



</summary><category term="async"/><category term="http"/><category term="python"/><category term="asgi"/><category term="kim-christie"/><category term="httpx"/></entry><entry><title>Usage of ARIA attributes via HTTP Archive</title><link href="https://simonwillison.net/2018/Jul/12/usage-aria-attributes-http-archive/#atom-tag" rel="alternate"/><published>2018-07-12T03:16:26+00:00</published><updated>2018-07-12T03:16:26+00:00</updated><id>https://simonwillison.net/2018/Jul/12/usage-aria-attributes-http-archive/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://discuss.httparchive.org/t/usage-of-aria-attributes/778"&gt;Usage of ARIA attributes via HTTP Archive&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
A neat example of a Google BigQuery query you can run against the HTTP Archive public dataset (a crawl of the “top” websites run periodically by the Internet Archive, which captures the full details of every resource fetched) to see which ARIA attributes are used the most often. Linking to this because I used it successfully today as the basis for my own custom query—I love that it’s possible to analyze a huge representative sample of the modern web in this way.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/aria"&gt;aria&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/internet-archive"&gt;internet-archive&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/big-data"&gt;big-data&lt;/a&gt;&lt;/p&gt;



</summary><category term="aria"/><category term="http"/><category term="internet-archive"/><category term="big-data"/></entry><entry><title>Cookies-over-HTTP Bad</title><link href="https://simonwillison.net/2018/Apr/7/cookies-over-http-bad/#atom-tag" rel="alternate"/><published>2018-04-07T14:39:06+00:00</published><updated>2018-04-07T14:39:06+00:00</updated><id>https://simonwillison.net/2018/Apr/7/cookies-over-http-bad/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/mikewest/cookies-over-http-bad"&gt;Cookies-over-HTTP Bad&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Mike West from the Chrome security team proposes a way for browsers to start discouraging the use of tracking cookies sent over HTTP—which represent a significant threat to user privacy from network attackers. It’s a clever piece of thinking: browsers would slowly ramp up the forced expiry deadline for non-HTTPS cookies, further encouraging sites to switch to HTTPS cookies while giving them ample time to adapt.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/mikewest/status/982253957291524097"&gt;@mikewest&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/cookies"&gt;cookies&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/https"&gt;https&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/privacy"&gt;privacy&lt;/a&gt;&lt;/p&gt;



</summary><category term="cookies"/><category term="http"/><category term="https"/><category term="privacy"/></entry></feed>