Files
xarmian fa66181b88 fix(webhooks): a DNS failure is retried, and a redirect may not turn the POST into a bodyless GET (BUG-3661, BUG-3662) (#2163)
* fix(webhooks): a DNS failure is retried, and a redirect may not turn the POST into a bodyless GET (BUG-3661, BUG-3662)

BUG-3661: the owner path's pre-delivery URL check returned the resolver's
error like a policy refusal, and attemptDeliver and checkRedirect made every
validation error permanent, so a DNS timeout or SERVFAIL ended the endpoint's
part in the event for good. The app path's dial turned resolver errors into
ErrRefused, which attemptApp maps to permanent. Now ValidateWebhookURLContext
returns a typed ResolveError from a lookup bound to the attempt's timeout (a
seam, lookupIP), attemptDeliver and checkRedirect treat it as transient, and
appfetch's dial returns resolver errors as a network error (ErrNotSent from
Poster.Post: transient, unsent). Policy refusals stay permanent.

BUG-3662: net/http follows a 301, 302 or 303 after a POST as a bodyless GET
and keeps the headers, so the receiver got a GET with a signature for a body
it never saw, and a 2xx was recorded as delivered. checkRedirect now refuses a
hop that changes the method or drops the body (permanent: the receiver's
configuration). 307 and 308 keep both and are followed.

Tests: resolver failure transient at delivery, at a redirect and on the app
poster, with policy controls staying permanent / refused; 301/302/303 refused
without sending the GET; 307/308 deliver the POST with its body and signature.
Each fix's mutant turns its test red.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(webhooks): a dial the address screen refuses is permanent, and one deadline covers the whole attempt (BUG-3661, codex r1)

Medium: screenDialAddr refusals (a DNS answer that changed between the
URL check and the dial) were plain errors, so the Do error counted as
transient and a policy refusal was retried. They now wrap errDialBlocked,
which attemptDeliver treats as permanent. Low: the pre-delivery lookup had
its own deliveryTimeout before the request got another; both now run under
one attempt context. TestBUG3661_DialTimeBlockIsPermanent (validation sees a
public answer, the dialer resolves localhost) is red with the classification
removed.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-14 10:05:08 -04:00
..