fix(forge): retry 5xx server errors at the HTTP client level - #2342
Conversation
Move 5xx retry handling from the higher-level retryOnTransient wrapper (now renamed retryOnRepoRace) down into isRetryable, which is used by do(). This ensures all GitHub API calls automatically retry on transient server errors (500-504), not just the handful of call sites that were wrapped in retryOnTransient. This fixes a 502 Bad Gateway failure in post-review's GetPullRequestHeadSHA, which had no retry coverage because it called get() directly. Rename retryOnTransient to retryOnRepoRace and narrow isTransientStatus to only cover 404 (async repo init) and 409 (branch ref conflict), which are the race conditions that wrapper actually exists for. Assisted-by: Claude claude-opus-4-6 <noreply@anthropic.com> Signed-off-by: Ralph Bean <rbean@redhat.com>
Site previewPreview: https://8ae8c2d3-site.fullsend-ai.workers.dev Commit: |
|
🤖 Finished Review · ✅ Success · Started 4:20 PM UTC · Completed 4:32 PM UTC |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
ReviewFindingsMedium
Low
Info
Labels: PR fixes a 502 Bad Gateway retry bug in the forge GitHub client. Previous runReviewFindingsCritical
Low
Info
Previous run (2)ReviewFindingsMedium
Low
Info
|
|
🤖 Finished Review · ✅ Success · Started 3:28 PM UTC · Completed 3:40 PM UTC |
| // With a persistent 504 on PUT, do() exhausts its 3 attempts and | ||
| // returns immediately — retryOnRepoRace does not retry 5xx. | ||
| callNum := 0 | ||
| srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
There was a problem hiding this comment.
[low] test-adequacy
TestCreateOrUpdateFile_MaxRetriesExceeded should verify the total HTTP call count to confirm the expected retry behavior matches the new architecture where do() handles 5xx retries.
| // only covers race-condition statuses (404 async repo init, 409 ref conflict). | ||
| transient := []int{404, 409} | ||
| for _, code := range transient { | ||
| assert.True(t, isTransientStatus(code), "expected %d to be transient", code) |
There was a problem hiding this comment.
[low] test-adequacy
TestDo_RetriesOnServerError incurs real wall-clock delay from the exponential backoff in do(), with no overridable hook (unlike secondaryRateLimitBackoff). This will accumulate test-suite runtime.
| @@ -146,7 +146,7 @@ func (c *LiveClient) do(ctx context.Context, method, path string, body any) (*ht | |||
| retryAfter := resp.Header.Get("Retry-After") | |||
There was a problem hiding this comment.
[low] scope-coherence
The PR uses fix(forge) prefix but includes architectural refactoring. The fix prefix is defensible since the change addresses a concrete gap where GetPullRequestHeadSHA had no 5xx retry coverage.
| @@ -591,16 +597,13 @@ func (c *LiveClient) retryOnTransient(ctx context.Context, label string, fn func | |||
| } | |||
There was a problem hiding this comment.
[info] naming-alignment
isTransientStatus now only checks 404/409 repo race conditions. Consider renaming to isRepoRaceStatus for full consistency with retryOnRepoRace.
| @@ -146,7 +146,7 @@ func (c *LiveClient) do(ctx context.Context, method, path string, body any) (*ht | |||
| retryAfter := resp.Header.Get("Retry-After") | |||
|
|
|||
| if attempt == maxRetries-1 { | |||
There was a problem hiding this comment.
[info] terminology-consistency
Error message uses 'attempts' but the constant is maxRetries. The semantics are correct (maxRetries=3 means 3 attempts).
| @@ -146,7 +146,7 @@ func (c *LiveClient) do(ctx context.Context, method, path string, body any) (*ht | |||
| retryAfter := resp.Header.Get("Retry-After") | |||
|
|
|||
| if attempt == maxRetries-1 { | |||
There was a problem hiding this comment.
[info] error-handling-gap
The error message format string has a missing closing parenthesis in the base string, with ) appended later via concatenation. Output is correct but the pattern is fragile. Pre-existing, not introduced by this PR.
Two call sites in commitFilesTo were missed during the rename, causing build failures. Assisted-by: Claude claude-opus-4-6 <noreply@anthropic.com> Signed-off-by: Ralph Bean <rbean@redhat.com>
|
🤖 Finished Review · ❌ Failure · Started 4:21 PM UTC · Completed 4:33 PM UTC |
|
🤖 Finished Retro · ✅ Success · Started 5:56 PM UTC · Completed 6:05 PM UTC |
Retro: PR #2342 — fix(forge): retry 5xx server errors at the HTTP client levelWhat happenedHuman-authored PR by Timeline:
AssessmentReview quality was strong. The critical compilation error catch on the second review was exactly the kind of finding that justifies automated reviews — it prevented a broken build from merging. Three patterns worth noting, all covered by existing issues:
No new proposals filed — all identified improvements are already tracked in existing open issues. |
Summary
retryOnTransientdown intoisRetryableindo(), so all GitHub API calls automatically retry on transient server errorsretryOnTransient→retryOnRepoRaceand narrowsisTransientStatusto only 404/409, reflecting its actual purpose (repo init races and branch ref conflicts)GetPullRequestHeadSHAhad no retry coverageTest plan
TestIsRetryable_ServerErrors— verifiesisRetryablereturns true for 500/502/503/504TestDo_RetriesOnServerError— verifiesdo()retries a 502 and succeeds on next attemptdo(), not at the wrapper level)./internal/forge/...test suite passes🤖 Generated with Claude Code