Skip to content

fix(generator)!: a redirect refusal is never retried, whatever its message or wrapping - #147

Merged
Michael McQuade (giraffesyo) merged 4 commits into
canaryfrom
refactor/retry-redirect-prefix
Oct 26, 2026
Merged

Michael McQuade (giraffesyo) merged 4 commits into
canaryfrom
refactor/retry-redirect-prefix

Conversation

@giraffesyo

@giraffesyo Michael McQuade (giraffesyo) commented Oct 26, 2026 •

Copy link
Copy Markdown
Member

A generated client could retry a redirect its policy had refused. The retry recognized a refusal only by an error message starting "stopped after", or by a response returned with the error. A caller's own CheckRedirect worded otherwise, behind a middleware that drops the response, was sent again.

What changes:

  • redirect.go gains an exported RedirectRefusedError{Err error} with Unwrap. Its message is the refusal's own, so error text is unchanged, and errors.Is/errors.As on the original error still work.
  • SafeRedirects always returns a copy of the client whose CheckRedirect wraps the effective policy: the caller's own when set, otherwise the generated CheckRedirect. The caller's policy still decides every redirect. It is wrapped, never replaced. http.ErrUseLastResponse passes through unwrapped, because net/http compares it with ==.
  • isRetryableNetworkError declines a *RedirectRefusedError and drops the "stopped after" prefix check. send also declines when Client.Do returns a response along with the error, which is net/http's signal for a CheckRedirect refusal.
  • RedirectRefusedError is added to the reserved identifiers.

Tests in e2e_redirect_test.go cover:

  • a caller's refusal with any message, or wrapping ErrInsecureRedirect, is tried once;
  • a caller's refusal behind a middleware that drops the response is tried once and comes back as a *RedirectRefusedError wrapping the caller's error;
  • the client's own ErrInsecureRedirect and ErrTooManyRedirects are *RedirectRefusedErrors;
  • ErrTooManyRedirects behind a response-dropping middleware is not retried;
  • a caller's policy that follows a redirect the generated policy would stop still follows it, and one that returns ErrUseLastResponse is kept.

Removing the wrap in SafeRedirects fails the new tests. go test ./..., go vet ./... and gofmt -l . are clean.

Breaking

A redirect refusal now arrives wrapped in a *RedirectRefusedError, and SafeRedirects always returns a copy of the client. Code that compares a refusal with == (such as urlErr.Err == ErrInsecureRedirect), checks what errors.Unwrap returns, uses a type switch on its own CheckRedirect error, or expects SafeRedirects(c) == c should use errors.Is or errors.As instead.

net/http wraps every CheckRedirect error in a *url.Error, so the "stopped after" prefix check already covers ErrInsecureRedirect and ErrTooManyRedirects. The errors.Is check before it was a second path to the same answer.
…sage or wrapping

SafeRedirects now wraps the client's redirect policy, the generated CheckRedirect or the caller's own, so each refusal comes back as a *RedirectRefusedError that unwraps to the original error. The retry declines on that type instead of on a "stopped after" message prefix, which missed a caller's refusal worded otherwise once a middleware dropped its response. A caller's policy still decides every redirect, and http.ErrUseLastResponse passes through unwrapped.
@giraffesyo Michael McQuade (giraffesyo) changed the title refactor(generator): retry declines redirect refusals by one check fix(generator): a redirect refusal is never retried, whatever its message or wrapping Oct 26, 2026
Also drops a redundant test check and keeps the reserved identifiers in order.

BREAKING CHANGE: every redirect refusal, the generated CheckRedirect's or a caller's own, now comes back wrapped in a *RedirectRefusedError, and SafeRedirects always returns a copy of the client. Code that compares a refusal with ==, as in urlErr.Err == ErrInsecureRedirect, checks the result of errors.Unwrap, uses a type switch on its own CheckRedirect error, or expects SafeRedirects(c) == c must use errors.Is or errors.As instead.
@giraffesyo Michael McQuade (giraffesyo) changed the title fix(generator): a redirect refusal is never retried, whatever its message or wrapping fix(generator)!: a redirect refusal is never retried, whatever its message or wrapping Oct 26, 2026
@giraffesyo
Michael McQuade (giraffesyo) marked this pull request as ready for review October 26, 2026 03:32
@giraffesyo
Michael McQuade (giraffesyo) merged commit 5a5da04 into canary Oct 26, 2026
8 checks passed
@giraffesyo
Michael McQuade (giraffesyo) deleted the refactor/retry-redirect-prefix branch October 26, 2026 14:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant