Skip to content

Commit a4333f3

Browse files
authored
Use a max retry after duration for secondary rate limit if specified (#3438)
1 parent 8bd68fe commit a4333f3

File tree

2 files changed

+49
-0
lines changed

2 files changed

+49
-0
lines changed

github/github.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,10 @@ type Client struct {
176176
rateLimits [Categories]Rate // Rate limits for the client as determined by the most recent API calls.
177177
secondaryRateLimitReset time.Time // Secondary rate limit reset for the client as determined by the most recent API calls.
178178

179+
// If specified, Client will block requests for at most this duration in case of reaching a secondary
180+
// rate limit
181+
MaxSecondaryRateLimitRetryAfterDuration time.Duration
182+
179183
// Whether to respect rate limit headers on endpoints that return 302 redirections to artifacts
180184
RateLimitRedirectionalEndpoints bool
181185

@@ -928,6 +932,10 @@ func (c *Client) bareDo(ctx context.Context, caller *http.Client, req *http.Requ
928932
// Update the secondary rate limit if we hit it.
929933
rerr, ok := err.(*AbuseRateLimitError)
930934
if ok && rerr.RetryAfter != nil {
935+
// if a max duration is specified, make sure that we are waiting at most this duration
936+
if c.MaxSecondaryRateLimitRetryAfterDuration > 0 && rerr.GetRetryAfter() > c.MaxSecondaryRateLimitRetryAfterDuration {
937+
rerr.RetryAfter = &c.MaxSecondaryRateLimitRetryAfterDuration
938+
}
931939
c.rateMu.Lock()
932940
c.secondaryRateLimitReset = time.Now().Add(*rerr.RetryAfter)
933941
c.rateMu.Unlock()

github/github_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1852,6 +1852,47 @@ func TestDo_rateLimit_abuseRateLimitError_xRateLimitReset(t *testing.T) {
18521852
}
18531853
}
18541854

1855+
// Ensure *AbuseRateLimitError.RetryAfter respect a max duration if specified.
1856+
func TestDo_rateLimit_abuseRateLimitError_maxDuration(t *testing.T) {
1857+
t.Parallel()
1858+
client, mux, _ := setup(t)
1859+
// specify a max retry after duration of 1 min
1860+
client.MaxSecondaryRateLimitRetryAfterDuration = 60 * time.Second
1861+
1862+
// x-ratelimit-reset value of 1h into the future, to make sure we are way over the max wait time duration.
1863+
blockUntil := time.Now().Add(1 * time.Hour).Unix()
1864+
1865+
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
1866+
w.Header().Set("Content-Type", "application/json; charset=utf-8")
1867+
w.Header().Set(headerRateReset, strconv.Itoa(int(blockUntil)))
1868+
w.Header().Set(headerRateRemaining, "1") // set remaining to a value > 0 to distinct from a primary rate limit
1869+
w.WriteHeader(http.StatusForbidden)
1870+
fmt.Fprintln(w, `{
1871+
"message": "You have triggered an abuse detection mechanism ...",
1872+
"documentation_url": "https://docs.github.com/en/rest/overview/resources-in-the-rest-api#abuse-rate-limits"
1873+
}`)
1874+
})
1875+
1876+
req, _ := client.NewRequest("GET", ".", nil)
1877+
ctx := context.Background()
1878+
_, err := client.Do(ctx, req, nil)
1879+
1880+
if err == nil {
1881+
t.Error("Expected error to be returned.")
1882+
}
1883+
abuseRateLimitErr, ok := err.(*AbuseRateLimitError)
1884+
if !ok {
1885+
t.Fatalf("Expected a *AbuseRateLimitError error; got %#v.", err)
1886+
}
1887+
if abuseRateLimitErr.RetryAfter == nil {
1888+
t.Fatalf("abuseRateLimitErr RetryAfter is nil, expected not-nil")
1889+
}
1890+
// check that the retry after is set to be the max allowed duration
1891+
if got, want := *abuseRateLimitErr.RetryAfter, client.MaxSecondaryRateLimitRetryAfterDuration; got != want {
1892+
t.Errorf("abuseRateLimitErr RetryAfter = %v, want %v", got, want)
1893+
}
1894+
}
1895+
18551896
func TestDo_noContent(t *testing.T) {
18561897
t.Parallel()
18571898
client, mux, _ := setup(t)

0 commit comments

Comments
 (0)