fix(github): address review feedback on Retry-After implementation
PR Ready Gate / clear-labels (pull_request) Successful in 2s
CI / test (pull_request) Successful in 23s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 43s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 1m21s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 2m22s

- Fix data race: copy retryBackoff slice per-request to prevent
  concurrent doRequest calls from racing on shared state
- Fix parseRetryAfter: trim whitespace before parsing for robustness
- Fix parseRetryAfter: treat delta-seconds of 0 as valid per RFC 7231
- Add bounded read on success path (10 MB limit) for defense-in-depth
- Clean up TestDoRequest_429_RetryAfter_IntegerSeconds: remove dead
  code block and commented-out reasoning
- Fix import ordering: context before errors (goimports compliance)
This commit is contained in:
claw
2026-05-13 05:54:06 -07:00
parent 41e1d48b54
commit e414471a16
2 changed files with 22 additions and 53 deletions
+9 -49
View File
@@ -1,8 +1,8 @@
package github
import (
"errors"
"context"
"errors"
"net/http"
"net/http/httptest"
"testing"
@@ -48,7 +48,7 @@ func TestDoRequest_429_RetryAfter_IntegerSeconds(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
attempts++
if attempts == 1 {
w.Header().Set("Retry-After", "3")
w.Header().Set("Retry-After", "1")
w.WriteHeader(http.StatusTooManyRequests)
w.Write([]byte("rate limited"))
return
@@ -59,52 +59,9 @@ func TestDoRequest_429_RetryAfter_IntegerSeconds(t *testing.T) {
defer srv.Close()
c := NewClient("tok", srv.URL)
// Use zero backoff so test doesn't wait — the Retry-After override only
// affects backoff[attempt] which is used on the NEXT iteration. Since
// we only have one retry, we set backoff[0] to 0 initially, then
// the 429 handler overrides it. To avoid waiting, we cancel quickly.
// Actually: the flow is attempt=0 gets 429, handler overrides backoff[0],
// then attempt=1 reads backoff[0]. So we need backoff[0] to be small after override.
// With Retry-After: 3, backoff[0] becomes 3s. Let's use context timeout.
// Better approach: just set backoff large enough and use very short timeout.
// Simplest: verify parsing works via parseRetryAfter unit tests and keep
// the integration test fast by not actually waiting.
// For integration: set backoff to 0 initially. The 429 handler will override
// backoff[0] to 3s. To avoid waiting 3s, we'll just verify it retried.
// Actually we need to accept the 3s wait OR use a different test strategy.
// Best approach: use a 1ms initial backoff that gets overridden, but we
// check correctness via the parseRetryAfter unit tests. For the integration
// test, use Retry-After: 0 edge case OR just test that retry happens.
c.SetRetryBackoff([]time.Duration{0, 0})
// The handler sets Retry-After: 3, which will override backoff[0] to 3s.
// But since we start with backoff[0]=0, the first attempt runs immediately,
// then on 429 the code does: backoff[0] = 3s. The retry loop then uses
// backoff[attempt-1] = backoff[0] = 3s for the delay before attempt 1.
// To keep the test fast, let's just test a small value.
srv.Close()
// Recreate with small Retry-After
attempts = 0
srv2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
attempts++
if attempts == 1 {
w.Header().Set("Retry-After", "1")
w.WriteHeader(http.StatusTooManyRequests)
w.Write([]byte("rate limited"))
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("success"))
}))
defer srv2.Close()
c2 := NewClient("tok", srv2.URL)
c2.SetRetryBackoff([]time.Duration{0, 0})
body, err := c2.doGet(context.Background(), srv2.URL+"/test")
body, err := c.doGet(context.Background(), srv.URL+"/test")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -320,9 +277,12 @@ func TestParseRetryAfter_IntegerSeconds(t *testing.T) {
func TestParseRetryAfter_ZeroSeconds(t *testing.T) {
c := NewClient("tok", "")
_, ok := c.parseRetryAfter("0")
if ok {
t.Error("expected ok=false for zero seconds")
delay, ok := c.parseRetryAfter("0")
if !ok {
t.Fatal("expected ok=true for zero seconds (RFC 7231 allows immediate retry)")
}
if delay != 0 {
t.Errorf("delay = %v, want 0", delay)
}
}