Webhooks: a redirected POST is re-sent as a GET with no body, and can be recorded as delivered #550
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#550
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-09-29 in a read-only review of postulo at
95348908d, reading the webhook plugin and posting through it to a mock transport that answers 301.Related to #240.
Checked again on 2026-10-01 against
mainat0a5cad587: still present. The code is now atplugins/http.py:143-144(_build); the webhook plugin andnotifications/webhooks.pyare where they were. The same post repeated on today's code again produced aPOSTwith the body and then aGETwithout one.webhook.postsends throughclient(), which follows redirects by default. On a 301, 302 or 303 httpx re-issues the request as aGETwithout the body but with thePostulo-Signature, event and delivery headers, anddeliver()looks only at the final status. The event is then either recorded as sent without ever having been delivered, or given up with a status that says nothing about the redirect.plugins/webhook/__init__.py:232-242(post);plugins/http.py:141-142(_build:follow_redirectsdefaults toTrue, up to three hops);notifications/webhooks.py:137-152(deliverjudges the final response only);plugins/webhook/__init__.py:216-229(WebhookPlugin.test, same).http://hooks.example.org/into a receiver that answers301 Location: https://hooks.example.org/inproduced two requests:POST http://hooks.example.org/inwith the 7-byte body, thenGET https://hooks.example.org/inwith a zero-length body and the same signature header. If the receiver answers theGETwith 200, the row isSENTand the Test button says "Answered 200". If it answers 405 or 404, the row is given up at once as a client error ("The receiver answered 405."), with nothing pointing at the redirect.http://address in front of a proxy that upgrades to HTTPS with a 301, or a receiver that moved its path, is an ordinary configuration. The person's automation never receives a payload while the history shows success, or they chase a 405 that their receiver never saw for aPOST.follow_redirects=Falseinwebhook.post. Indeliverand inWebhookPlugin.test, treat any 3xx as a failure whose message names theLocation("The receiver redirected to %(location)s; use that address instead."), final for 301 and 308 so the person corrects the address, transient for the others.Done when
tests/test_webhooks.pydelivers a row to a receiver stubbed to answer 301 and then 200, and asserts exactly one request was made, the row is notSENT, andlast_errornames the redirect target.tests/test_webhooks.pyassertsWebhookPlugin.testreports the redirect rather than "Answered 200".