Problem/Motivation
McpWebhookWorker sends deliveries with Guzzle's default redirect behavior. When a configured endpoint URL answers with a 301/302 (the classic case: apex domain canonicalizing to www at the edge), Guzzle follows the redirect and — per RFC-compliant client behavior — re-issues the request as a GET with no body. The receiver at the redirect target correctly refuses it (405 or similar), the delivery walks the whole retry/backoff ladder, and after MAX_ATTEMPTS the row lands in permanent failed.
Nothing anywhere mentions the redirect: last_response_code records the terminal 405 from the wrong request, and the operator is left staring at a receiver that answers POSTs fine when tested by hand. Observed in production: 1968 of 1968 deliveries over a month failed exactly this way because the endpoint was configured with the apex URL.
A signed webhook cannot survive a redirect hop. The method gets rewritten, the HMAC-signed body is dropped, and the request that arrives is not the request that was signed. Receivers must be addressed by their exact URL.
Steps to reproduce
- Configure a webhook endpoint whose URL 301-redirects (e.g.
https://example.com/hook→https://www.example.com/hook). - Trigger any subscribed event.
- The delivery fails with the target's 405/4xx, retries five times over ~11 hours, then permanently fails. No log or delivery-row field ever mentions the redirect.
Proposed resolution
- Send with
'allow_redirects' => FALSE. The worker should never follow a redirect for a signed delivery. - Treat any 3xx response as a delivery failure and record the
Locationheader inlast_response_body, so the delivery log says "endpoint redirects to X — configure the exact receiver URL" instead of a bare 405.
Related hardening noticed while diagnosing (split into separate issues if preferred)
- A row claimed
in_progressby a worker that dies before writing a result is stranded forever — the cron re-enqueue scan only picks uppending. A stale-claim sweep (reset topendingafter a claim TTL) would make the pipeline self-healing. - Permanent failures are invisible: nothing surfaces the count of
faileddelivery rows. A hook_requirements() warning (and/or dashboard figure) when permanent failures accumulate would have turned a month of silence into a status-report red flag.
Comments
Comment #2
jmcerdaFix implemented: https://github.com/Wilkes-Liberty/mcp_sentinel/pull/53
allow_redirects => FALSEon the delivery request; a 3xx answer fails terminally asfailed_redirectwith theLocationrecorded inlast_response_body, so the delivery log names the misconfigured URL instead of a bare 405 after five useless retries. Dashboard badge, filter form and metrics learn the new status.in_progressby a dead worker (claim TTL 1h, attempt counter bumped so a poisoned row still converges on MAX_ATTEMPTS), and hook_requirements() warns on the status report when permanent failures accumulate — count, newest failure time, delivery-log link.allow_redirects === falseasserted on the captured request; replay is a no-op), stale-claim reclaim respects the TTL, and the requirements warning appears exactly when failure rows exist.Follow-up observation from the production incident that motivated this: the signing Key resolved to an empty value there, so deliveries were also going out unsigned — the worker only adds
X-MCP-Signaturewhen the secret is non-empty. Worth a separate issue on whether an endpoint configured with a signing key whose value is empty should refuse to send (fail closed) rather than silently send unsigned.Comment #3
jmcerdaMerged to
1.x(PR #53, merge commit 90e9d70). All CI legs green (coding standards + PHPUnit on Drupal 10.6/11/11.3). Ships in 1.12.0 together with #3613146; changelog entries are finalized in that release. The unsigned-when-key-empty follow-up noted in #2 remains open for a separate issue.Comment #5
jmcerdaShipped in 1.13.0. Closing after release.