Webhooks: a redirected POST is re-sent as a GET with no body, and can be recorded as delivered #550

Closed
opened 2026-10-01 10:47:34 +00:00 by tiagoagueda · 0 comments
Owner

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 main at 0a5cad587: still present. The code is now at plugins/http.py:143-144 (_build); the webhook plugin and notifications/webhooks.py are where they were. The same post repeated on today's code again produced a POST with the body and then a GET without one.

webhook.post sends through client(), which follows redirects by default. On a 301, 302 or 303 httpx re-issues the request as a GET without the body but with the Postulo-Signature, event and delivery headers, and deliver() 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.

  • Where: plugins/webhook/__init__.py:232-242 (post); plugins/http.py:141-142 (_build: follow_redirects defaults to True, up to three hops); notifications/webhooks.py:137-152 (deliver judges the final response only); plugins/webhook/__init__.py:216-229 (WebhookPlugin.test, same).
  • What happens: posting http://hooks.example.org/in to a receiver that answers 301 Location: https://hooks.example.org/in produced two requests: POST http://hooks.example.org/in with the 7-byte body, then GET https://hooks.example.org/in with a zero-length body and the same signature header. If the receiver answers the GET with 200, the row is SENT and 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.
  • Why it matters: an 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 a POST.
  • Fix: post with follow_redirects=False in webhook.post. In deliver and in WebhookPlugin.test, treat any 3xx as a failure whose message names the Location ("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

  • A test in tests/test_webhooks.py delivers a row to a receiver stubbed to answer 301 and then 200, and asserts exactly one request was made, the row is not SENT, and last_error names the redirect target.
  • A test in tests/test_webhooks.py asserts WebhookPlugin.test reports the redirect rather than "Answered 200".
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 `main` at `0a5cad587`: still present. The code is now at `plugins/http.py:143-144` (`_build`); the webhook plugin and `notifications/webhooks.py` are where they were. The same post repeated on today's code again produced a `POST` with the body and then a `GET` without one. `webhook.post` sends through `client()`, which follows redirects by default. On a 301, 302 or 303 httpx re-issues the request as a `GET` without the body but with the `Postulo-Signature`, event and delivery headers, and `deliver()` 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. - **Where:** `plugins/webhook/__init__.py:232-242` (`post`); `plugins/http.py:141-142` (`_build`: `follow_redirects` defaults to `True`, up to three hops); `notifications/webhooks.py:137-152` (`deliver` judges the final response only); `plugins/webhook/__init__.py:216-229` (`WebhookPlugin.test`, same). - **What happens:** posting `http://hooks.example.org/in` to a receiver that answers `301 Location: https://hooks.example.org/in` produced two requests: `POST http://hooks.example.org/in` with the 7-byte body, then `GET https://hooks.example.org/in` with a zero-length body and the same signature header. If the receiver answers the `GET` with 200, the row is `SENT` and 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. - **Why it matters:** an `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 a `POST`. - **Fix:** post with `follow_redirects=False` in `webhook.post`. In `deliver` and in `WebhookPlugin.test`, treat any 3xx as a failure whose message names the `Location` ("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 - A test in `tests/test_webhooks.py` delivers a row to a receiver stubbed to answer 301 and then 200, and asserts exactly one request was made, the row is not `SENT`, and `last_error` names the redirect target. - A test in `tests/test_webhooks.py` asserts `WebhookPlugin.test` reports the redirect rather than "Answered 200".
tiagoagueda added this to the 0.6.0 milestone 2026-10-01 10:47:34 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#550
No description provided.