Auth: retry the token exchange once in place before restarting

What does this MR do and why?

GitLab returns one identical invalid_grant for a spent code and for a grant that a lagging database replica has not seen yet, and a lost connection leaves the code unspent. In both cases a second attempt with the same code succeeds where !1309 (closed)'s restart costs a browser round trip. exchangeCode now retries once after 250 ms for transport errors other than timeouts, upstream 5xx and invalid_grant. A spent code fails the retry within milliseconds and the existing restart takes over.

  • retryInPlace names the policy per failure class.
  • gitlab_pages_auth_token_fetch_failures_total gains the retried outcome; gitlab_pages_auth_token_fetch_recoveries_total{via} records successes after a failure.
  • Only terminal failures log at error level.

Acceptance harness against the stub's contract mode, with two scenarios: a grant not yet visible and a dropped connection, each recovering with exactly two exchanges and nothing shown to the user. The earlier restart-recovery acceptance test is superseded, since the in-place retry recovers its scenario before any restart.

Part of a stack replacing !1333 (closed) for #1190 (closed), on top of !1309 (closed):

  1. stub contract mode (test-only)
  2. move token-fetch handling into its own file
  3. retry the exchange once in place
  4. bounded restart with a recovery cookie
  5. failure page at the requested page

Each MR targets the previous one's branch and retargets automatically when that merges.

How to validate

go test ./internal/auth/ and make acceptance ARGS='-run TestSignIn'.

🤖 Generated with Claude Code

Merge request reports

Loading
Loading