address review feedback: baseURL TODO, timer-cancel test, fast retry test, doc note
PR Ready Gate / clear-labels (pull_request) Successful in 1s
CI / test (pull_request) Successful in 21s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 49s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m47s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 2m14s
PR Ready Gate / clear-labels (pull_request) Successful in 1s
CI / test (pull_request) Successful in 21s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 49s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m47s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 2m14s
- Add TODO comment on unused baseURL field explaining its intended future use by higher-level exported methods - Rewrite TestDoRequest_ContextCanceled to actually exercise the timer-cancel path in the retry select (goroutine cancels context while client is blocked in timer.C select) - Change Retry-After: 1 to Retry-After: 0 in IntegerSeconds test to avoid unnecessary 1s sleep during test runs - Add doc note on SetRetryBackoff explaining that an empty non-nil slice silently drops Retry-After delays
This commit is contained in:
@@ -89,6 +89,10 @@ func asAPIError(err error) (*APIError, bool) {
|
|||||||
// SetHTTPClient and SetRetryBackoff are intended for test setup only and must
|
// SetHTTPClient and SetRetryBackoff are intended for test setup only and must
|
||||||
// be called before any goroutines issue requests; they have no synchronization.
|
// be called before any goroutines issue requests; they have no synchronization.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
|
// TODO: baseURL is populated by NewClient but not yet consumed by doRequest/doGet.
|
||||||
|
// Higher-level exported methods (GetPullRequest, etc.) will use it to
|
||||||
|
// construct request URLs; remove this field if those methods end up
|
||||||
|
// accepting full URLs instead.
|
||||||
baseURL string
|
baseURL string
|
||||||
token string
|
token string
|
||||||
http *http.Client
|
http *http.Client
|
||||||
@@ -126,6 +130,11 @@ func (c *Client) SetHTTPClient(hc *http.Client) {
|
|||||||
|
|
||||||
// SetRetryBackoff sets the delays between retry attempts.
|
// SetRetryBackoff sets the delays between retry attempts.
|
||||||
// This is intended for testing to speed up retry tests.
|
// This is intended for testing to speed up retry tests.
|
||||||
|
//
|
||||||
|
// Note: if an empty non-nil slice is provided, Retry-After delays parsed from
|
||||||
|
// server responses will be computed and capped but not applied (because
|
||||||
|
// attempt < len(backoff) is always false). This is acceptable for the
|
||||||
|
// test-only use case but callers should be aware of this edge case.
|
||||||
func (c *Client) SetRetryBackoff(backoff []time.Duration) {
|
func (c *Client) SetRetryBackoff(backoff []time.Duration) {
|
||||||
c.retryBackoff = backoff
|
c.retryBackoff = backoff
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-3
@@ -48,7 +48,7 @@ func TestDoRequest_429_RetryAfter_IntegerSeconds(t *testing.T) {
|
|||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
attempts++
|
attempts++
|
||||||
if attempts == 1 {
|
if attempts == 1 {
|
||||||
w.Header().Set("Retry-After", "1")
|
w.Header().Set("Retry-After", "0")
|
||||||
w.WriteHeader(http.StatusTooManyRequests)
|
w.WriteHeader(http.StatusTooManyRequests)
|
||||||
w.Write([]byte("rate limited"))
|
w.Write([]byte("rate limited"))
|
||||||
return
|
return
|
||||||
@@ -242,18 +242,36 @@ func TestDoRequest_401_NoRetry(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDoRequest_ContextCanceled(t *testing.T) {
|
func TestDoRequest_ContextCanceled(t *testing.T) {
|
||||||
|
// This test exercises the timer-cancel path in the retry select:
|
||||||
|
// select { case <-timer.C; case <-ctx.Done() }
|
||||||
|
// The server returns 429 with a long Retry-After, and we cancel the
|
||||||
|
// context shortly after the first response so that cancellation races
|
||||||
|
// against the timer rather than preventing the initial HTTP round-trip.
|
||||||
|
requestReceived := make(chan struct{}, 1)
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
select {
|
||||||
|
case requestReceived <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
w.Header().Set("Retry-After", "10")
|
w.Header().Set("Retry-After", "10")
|
||||||
w.WriteHeader(http.StatusTooManyRequests)
|
w.WriteHeader(http.StatusTooManyRequests)
|
||||||
}))
|
}))
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
c := NewClient("tok", srv.URL)
|
c := NewClient("tok", srv.URL)
|
||||||
c.SetRetryBackoff([]time.Duration{5 * time.Second, 5 * time.Second})
|
c.SetRetryBackoff([]time.Duration{10 * time.Second, 10 * time.Second})
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
// Cancel immediately so the retry timer is interrupted.
|
defer cancel()
|
||||||
|
|
||||||
|
// Cancel the context after the first request completes, while the
|
||||||
|
// client is blocked in the retry timer select.
|
||||||
|
go func() {
|
||||||
|
<-requestReceived
|
||||||
|
// Small delay to ensure we're inside the timer select.
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
cancel()
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
_, err := c.doGet(ctx, srv.URL+"/test")
|
_, err := c.doGet(ctx, srv.URL+"/test")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user