Skip to content

Commit d96da2e

Browse files
authored
feat: Add DisableRateLimitCheck option to client (#3607)
1 parent efe572e commit d96da2e

File tree

3 files changed

+151
-47
lines changed

3 files changed

+151
-47
lines changed

README.md

Lines changed: 45 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -189,54 +189,71 @@ using the installation ID of the GitHub app and authenticate with the OAuth meth
189189

190190
### Rate Limiting ###
191191

192-
GitHub imposes a rate limit on all API clients. Unauthenticated clients are
193-
limited to 60 requests per hour, while authenticated clients can make up to
194-
5,000 requests per hour. The Search API has a custom rate limit. Unauthenticated
195-
clients are limited to 10 requests per minute, while authenticated clients
196-
can make up to 30 requests per minute. To receive the higher rate limit when
197-
making calls that are not issued on behalf of a user,
198-
use `UnauthenticatedRateLimitedTransport`.
199-
200-
The returned `Response.Rate` value contains the rate limit information
192+
GitHub imposes rate limits on all API clients. The [primary rate limit](https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api#about-primary-rate-limits)
193+
is the limit to the number of REST API requests that a client can make within a
194+
specific amount of time. This limit helps prevent abuse and denial-of-service
195+
attacks, and ensures that the API remains available for all users. Some
196+
endpoints, like the search endpoints, have more restrictive limits.
197+
Unauthenticated clients may request public data but have a low rate limit,
198+
while authenticated clients have rate limits based on the client
199+
identity.
200+
201+
In addition to primary rate limits, GitHub enforces [secondary rate limits](https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api#about-secondary-rate-limits)
202+
in order to prevent abuse and keep the API available for all users.
203+
Secondary rate limits generally limit the number of concurrent requests that a
204+
client can make.
205+
206+
The client returned `Response.Rate` value contains the rate limit information
201207
from the most recent API call. If a recent enough response isn't
202-
available, you can use `RateLimits` to fetch the most up-to-date rate
203-
limit data for the client.
208+
available, you can use the client `RateLimits` service to fetch the most
209+
up-to-date rate limit data for the client.
204210

205-
To detect an API rate limit error, you can check if its type is `*github.RateLimitError`:
211+
To detect a primary API rate limit error, you can check if the error is a
212+
`RateLimitError`.
206213

207214
```go
208215
repos, _, err := client.Repositories.List(ctx, "", nil)
209-
if _, ok := err.(*github.RateLimitError); ok {
210-
log.Println("hit rate limit")
216+
var rateErr *github.RateLimitError
217+
if errors.As(err, &rateError) {
218+
log.Printf("hit primary rate limit, used %d of %d\n", rateErr.Rate.Used, rateErr.rate.Limit)
211219
}
212220
```
213221

214-
Learn more about GitHub rate limiting in
215-
["REST API endpoints for rate limits"](https://docs.github.com/en/rest/rate-limit).
216-
217-
In addition to these rate limits, GitHub imposes a secondary rate limit on all API clients.
218-
This rate limit prevents clients from making too many concurrent requests.
219-
220-
To detect an API secondary rate limit error, you can check if its type is `*github.AbuseRateLimitError`:
222+
To detect an API secondary rate limit error, you can check if the error is an
223+
`AbuseRateLimitError`.
221224

222225
```go
223226
repos, _, err := client.Repositories.List(ctx, "", nil)
224-
if _, ok := err.(*github.AbuseRateLimitError); ok {
225-
log.Println("hit secondary rate limit")
227+
var rateErr *github.AbuseRateLimitError
228+
if errors.As(err, &rateErr) {
229+
log.Printf("hit secondary rate limit, retry after %v\n", rateErr.RetryAfter)
226230
}
227231
```
228232

229-
Alternatively, you can block until the rate limit is reset by using the `context.WithValue` method:
233+
If you hit the primary rate limit, you can use the `SleepUntilPrimaryRateLimitResetWhenRateLimited`
234+
method to block until the rate limit is reset.
230235

231236
```go
232237
repos, _, err := client.Repositories.List(context.WithValue(ctx, github.SleepUntilPrimaryRateLimitResetWhenRateLimited, true), "", nil)
233238
```
234239

235-
You can use [gofri/go-github-ratelimit](https://github.com/gofri/go-github-ratelimit) to handle
236-
secondary rate limit sleep-and-retry for you, as well as primary rate limit abuse-prevention and callback triggering.
240+
If you need to make a request even if the rate limit has been hit you can use
241+
the `BypassRateLimitCheck` method to bypass the rate limit check and make the
242+
request anyway.
243+
244+
```go
245+
repos, _, err := client.Repositories.List(context.WithValue(ctx, github.BypassRateLimitCheck, true), "", nil)
246+
```
247+
248+
For more advanced use cases, you can use [gofri/go-github-ratelimit](https://github.com/gofri/go-github-ratelimit)
249+
which provides a middleware (`http.RoundTripper`) that handles both the primary
250+
rate limit and secondary rate limit for the GitHub API. In this case you can
251+
set the client `DisableRateLimitCheck` to `true` so the client doesn't track the rate limit usage.
237252

238-
Learn more about GitHub secondary rate limiting in
239-
["About secondary rate limits"](https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28#about-secondary-rate-limits).
253+
If the client is an [OAuth app](https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api#primary-rate-limit-for-oauth-apps)
254+
you can use the apps higher rate limit to request public data by using the
255+
`UnauthenticatedRateLimitedTransport` to make calls as the app instead of as
256+
the user.
240257

241258
### Accepted Status ###
242259

github/github.go

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,11 @@ type Client struct {
171171
// User agent used when communicating with the GitHub API.
172172
UserAgent string
173173

174+
// DisableRateLimitCheck stops the client checking for rate limits or tracking
175+
// them. This is different to setting BypassRateLimitCheck in the context,
176+
// as that still tracks the rate limits.
177+
DisableRateLimitCheck bool
178+
174179
rateMu sync.Mutex
175180
rateLimits [Categories]Rate // Rate limits for the client as determined by the most recent API calls.
176181
secondaryRateLimitReset time.Time // Secondary rate limit reset for the client as determined by the most recent API calls.
@@ -850,21 +855,26 @@ func (c *Client) bareDo(ctx context.Context, caller *http.Client, req *http.Requ
850855

851856
req = withContext(ctx, req)
852857

853-
rateLimitCategory := GetRateLimitCategory(req.Method, req.URL.Path)
858+
rateLimitCategory := CoreCategory
854859

855-
if bypass := ctx.Value(BypassRateLimitCheck); bypass == nil {
856-
// If we've hit rate limit, don't make further requests before Reset time.
857-
if err := c.checkRateLimitBeforeDo(req, rateLimitCategory); err != nil {
858-
return &Response{
859-
Response: err.Response,
860-
Rate: err.Rate,
861-
}, err
862-
}
863-
// If we've hit a secondary rate limit, don't make further requests before Retry After.
864-
if err := c.checkSecondaryRateLimitBeforeDo(req); err != nil {
865-
return &Response{
866-
Response: err.Response,
867-
}, err
860+
if !c.DisableRateLimitCheck {
861+
rateLimitCategory = GetRateLimitCategory(req.Method, req.URL.Path)
862+
863+
if bypass := ctx.Value(BypassRateLimitCheck); bypass == nil {
864+
// If we've hit rate limit, don't make further requests before Reset time.
865+
if err := c.checkRateLimitBeforeDo(req, rateLimitCategory); err != nil {
866+
return &Response{
867+
Response: err.Response,
868+
Rate: err.Rate,
869+
}, err
870+
}
871+
872+
// If we've hit a secondary rate limit, don't make further requests before Retry After.
873+
if err := c.checkSecondaryRateLimitBeforeDo(req); err != nil {
874+
return &Response{
875+
Response: err.Response,
876+
}, err
877+
}
868878
}
869879
}
870880

@@ -894,9 +904,10 @@ func (c *Client) bareDo(ctx context.Context, caller *http.Client, req *http.Requ
894904
return response, err
895905
}
896906

897-
// Don't update the rate limits if this was a cached response.
898-
// X-From-Cache is set by https://github.com/bartventer/httpcache
899-
if response.Header.Get("X-From-Cache") == "" {
907+
// Don't update the rate limits if the client has rate limits disabled or if
908+
// this was a cached response. The X-From-Cache is set by
909+
// https://github.com/bartventer/httpcache if it's enabled.
910+
if !c.DisableRateLimitCheck && response.Header.Get("X-From-Cache") == "" {
900911
c.rateMu.Lock()
901912
c.rateLimits[rateLimitCategory] = response.Rate
902913
c.rateMu.Unlock()
@@ -1586,8 +1597,8 @@ that need to use a higher rate limit associated with your OAuth application.
15861597
This will add the client id and secret as a base64-encoded string in the format
15871598
ClientID:ClientSecret and apply it as an "Authorization": "Basic" header.
15881599
1589-
See https://docs.github.com/rest/#unauthenticated-rate-limited-requests for
1590-
more information.
1600+
See https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api#primary-rate-limit-for-oauth-apps
1601+
for more information.
15911602
*/
15921603
type UnauthenticatedRateLimitedTransport struct {
15931604
// ClientID is the GitHub OAuth client ID of the current application, which

github/github_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,9 @@ func testNewRequestAndDoFailureCategory(t *testing.T, methodName string, client
251251
client.BaseURL.Path = "/api-v3/"
252252
client.rateLimits[category].Reset.Time = time.Now().Add(10 * time.Minute)
253253
resp, err = f()
254+
if client.DisableRateLimitCheck {
255+
return
256+
}
254257
if bypass := resp.Request.Context().Value(BypassRateLimitCheck); bypass != nil {
255258
return
256259
}
@@ -1912,6 +1915,79 @@ func TestDo_rateLimit_abuseRateLimitError_maxDuration(t *testing.T) {
19121915
}
19131916
}
19141917

1918+
// Make network call if client has disabled the rate limit check.
1919+
func TestDo_rateLimit_disableRateLimitCheck(t *testing.T) {
1920+
t.Parallel()
1921+
client, mux, _ := setup(t)
1922+
client.DisableRateLimitCheck = true
1923+
1924+
reset := time.Now().UTC().Add(60 * time.Second)
1925+
client.rateLimits[CoreCategory] = Rate{Limit: 5000, Remaining: 0, Reset: Timestamp{reset}}
1926+
requestCount := 0
1927+
mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
1928+
requestCount++
1929+
w.Header().Set(headerRateLimit, "5000")
1930+
w.Header().Set(headerRateRemaining, "5000")
1931+
w.Header().Set(headerRateUsed, "0")
1932+
w.Header().Set(headerRateReset, fmt.Sprint(reset.Add(time.Hour).Unix()))
1933+
w.Header().Set(headerRateResource, "core")
1934+
w.Header().Set("Content-Type", "application/json; charset=utf-8")
1935+
w.WriteHeader(http.StatusOK)
1936+
fmt.Fprintln(w, `{}`)
1937+
})
1938+
req, _ := client.NewRequest("GET", ".", nil)
1939+
ctx := context.Background()
1940+
resp, err := client.Do(ctx, req, nil)
1941+
if err != nil {
1942+
t.Errorf("Do returned unexpected error: %v", err)
1943+
}
1944+
if got, want := resp.StatusCode, http.StatusOK; got != want {
1945+
t.Errorf("Response status code = %v, want %v", got, want)
1946+
}
1947+
if got, want := requestCount, 1; got != want {
1948+
t.Errorf("Expected 1 request, got %d", got)
1949+
}
1950+
if got, want := client.rateLimits[CoreCategory].Remaining, 0; got != want {
1951+
t.Errorf("Expected 0 requests remaining, got %d", got)
1952+
}
1953+
}
1954+
1955+
// Make network call if client has bypassed the rate limit check.
1956+
func TestDo_rateLimit_bypassRateLimitCheck(t *testing.T) {
1957+
t.Parallel()
1958+
client, mux, _ := setup(t)
1959+
1960+
reset := time.Now().UTC().Add(60 * time.Second)
1961+
client.rateLimits[CoreCategory] = Rate{Limit: 5000, Remaining: 0, Reset: Timestamp{reset}}
1962+
requestCount := 0
1963+
mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
1964+
requestCount++
1965+
w.Header().Set(headerRateLimit, "5000")
1966+
w.Header().Set(headerRateRemaining, "5000")
1967+
w.Header().Set(headerRateUsed, "0")
1968+
w.Header().Set(headerRateReset, fmt.Sprint(reset.Add(time.Hour).Unix()))
1969+
w.Header().Set(headerRateResource, "core")
1970+
w.Header().Set("Content-Type", "application/json; charset=utf-8")
1971+
w.WriteHeader(http.StatusOK)
1972+
fmt.Fprintln(w, `{}`)
1973+
})
1974+
req, _ := client.NewRequest("GET", ".", nil)
1975+
ctx := context.Background()
1976+
resp, err := client.Do(context.WithValue(ctx, BypassRateLimitCheck, true), req, nil)
1977+
if err != nil {
1978+
t.Errorf("Do returned unexpected error: %v", err)
1979+
}
1980+
if got, want := resp.StatusCode, http.StatusOK; got != want {
1981+
t.Errorf("Response status code = %v, want %v", got, want)
1982+
}
1983+
if got, want := requestCount, 1; got != want {
1984+
t.Errorf("Expected 1 request, got %d", got)
1985+
}
1986+
if got, want := client.rateLimits[CoreCategory].Remaining, 5000; got != want {
1987+
t.Errorf("Expected 5000 requests remaining, got %d", got)
1988+
}
1989+
}
1990+
19151991
func TestDo_noContent(t *testing.T) {
19161992
t.Parallel()
19171993
client, mux, _ := setup(t)

0 commit comments

Comments
 (0)