Skip to content

Commit e8d69e7

Browse files
authored
Extend Rate and Rate Limiting with X-Ratelimit-Used and X-Ratelimit-Resource headers (#3453)
1 parent 3a72a02 commit e8d69e7

File tree

5 files changed

+143
-28
lines changed

5 files changed

+143
-28
lines changed

github/github-stringify_test.go

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

github/github.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ const (
3838
headerAPIVersion = "X-Github-Api-Version"
3939
headerRateLimit = "X-Ratelimit-Limit"
4040
headerRateRemaining = "X-Ratelimit-Remaining"
41+
headerRateUsed = "X-Ratelimit-Used"
4142
headerRateReset = "X-Ratelimit-Reset"
43+
headerRateResource = "X-Ratelimit-Resource"
4244
headerOTP = "X-Github-Otp"
4345
headerRetryAfter = "Retry-After"
4446

@@ -763,11 +765,17 @@ func parseRate(r *http.Response) Rate {
763765
if remaining := r.Header.Get(headerRateRemaining); remaining != "" {
764766
rate.Remaining, _ = strconv.Atoi(remaining)
765767
}
768+
if used := r.Header.Get(headerRateUsed); used != "" {
769+
rate.Used, _ = strconv.Atoi(used)
770+
}
766771
if reset := r.Header.Get(headerRateReset); reset != "" {
767772
if v, _ := strconv.ParseInt(reset, 10, 64); v != 0 {
768773
rate.Reset = Timestamp{time.Unix(v, 0)}
769774
}
770775
}
776+
if resource := r.Header.Get(headerRateResource); resource != "" {
777+
rate.Resource = resource
778+
}
771779
return rate
772780
}
773781

github/github_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1182,7 +1182,9 @@ func TestDo_rateLimit(t *testing.T) {
11821182
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
11831183
w.Header().Set(headerRateLimit, "60")
11841184
w.Header().Set(headerRateRemaining, "59")
1185+
w.Header().Set(headerRateUsed, "1")
11851186
w.Header().Set(headerRateReset, "1372700873")
1187+
w.Header().Set(headerRateResource, "core")
11861188
})
11871189

11881190
req, _ := client.NewRequest("GET", ".", nil)
@@ -1197,10 +1199,16 @@ func TestDo_rateLimit(t *testing.T) {
11971199
if got, want := resp.Rate.Remaining, 59; got != want {
11981200
t.Errorf("Client rate remaining = %v, want %v", got, want)
11991201
}
1202+
if got, want := resp.Rate.Used, 1; got != want {
1203+
t.Errorf("Client rate used = %v, want %v", got, want)
1204+
}
12001205
reset := time.Date(2013, time.July, 1, 17, 47, 53, 0, time.UTC)
12011206
if !resp.Rate.Reset.UTC().Equal(reset) {
12021207
t.Errorf("Client rate reset = %v, want %v", resp.Rate.Reset.UTC(), reset)
12031208
}
1209+
if got, want := resp.Rate.Resource, "core"; got != want {
1210+
t.Errorf("Client rate resource = %v, want %v", got, want)
1211+
}
12041212
}
12051213

12061214
func TestDo_rateLimitCategory(t *testing.T) {
@@ -1288,7 +1296,9 @@ func TestDo_rateLimit_errorResponse(t *testing.T) {
12881296
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
12891297
w.Header().Set(headerRateLimit, "60")
12901298
w.Header().Set(headerRateRemaining, "59")
1299+
w.Header().Set(headerRateUsed, "1")
12911300
w.Header().Set(headerRateReset, "1372700873")
1301+
w.Header().Set(headerRateResource, "core")
12921302
http.Error(w, "Bad Request", 400)
12931303
})
12941304

@@ -1307,10 +1317,16 @@ func TestDo_rateLimit_errorResponse(t *testing.T) {
13071317
if got, want := resp.Rate.Remaining, 59; got != want {
13081318
t.Errorf("Client rate remaining = %v, want %v", got, want)
13091319
}
1320+
if got, want := resp.Rate.Used, 1; got != want {
1321+
t.Errorf("Client rate used = %v, want %v", got, want)
1322+
}
13101323
reset := time.Date(2013, time.July, 1, 17, 47, 53, 0, time.UTC)
13111324
if !resp.Rate.Reset.UTC().Equal(reset) {
13121325
t.Errorf("Client rate reset = %v, want %v", resp.Rate.Reset, reset)
13131326
}
1327+
if got, want := resp.Rate.Resource, "core"; got != want {
1328+
t.Errorf("Client rate resource = %v, want %v", got, want)
1329+
}
13141330
}
13151331

13161332
// Ensure *RateLimitError is returned when API rate limit is exceeded.
@@ -1321,7 +1337,9 @@ func TestDo_rateLimit_rateLimitError(t *testing.T) {
13211337
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
13221338
w.Header().Set(headerRateLimit, "60")
13231339
w.Header().Set(headerRateRemaining, "0")
1340+
w.Header().Set(headerRateUsed, "60")
13241341
w.Header().Set(headerRateReset, "1372700873")
1342+
w.Header().Set(headerRateResource, "core")
13251343
w.Header().Set("Content-Type", "application/json; charset=utf-8")
13261344
w.WriteHeader(http.StatusForbidden)
13271345
fmt.Fprintln(w, `{
@@ -1347,10 +1365,16 @@ func TestDo_rateLimit_rateLimitError(t *testing.T) {
13471365
if got, want := rateLimitErr.Rate.Remaining, 0; got != want {
13481366
t.Errorf("rateLimitErr rate remaining = %v, want %v", got, want)
13491367
}
1368+
if got, want := rateLimitErr.Rate.Used, 60; got != want {
1369+
t.Errorf("rateLimitErr rate used = %v, want %v", got, want)
1370+
}
13501371
reset := time.Date(2013, time.July, 1, 17, 47, 53, 0, time.UTC)
13511372
if !rateLimitErr.Rate.Reset.UTC().Equal(reset) {
13521373
t.Errorf("rateLimitErr rate reset = %v, want %v", rateLimitErr.Rate.Reset.UTC(), reset)
13531374
}
1375+
if got, want := rateLimitErr.Rate.Resource, "core"; got != want {
1376+
t.Errorf("rateLimitErr rate resource = %v, want %v", got, want)
1377+
}
13541378
}
13551379

13561380
// Ensure a network call is not made when it's known that API rate limit is still exceeded.
@@ -1363,7 +1387,9 @@ func TestDo_rateLimit_noNetworkCall(t *testing.T) {
13631387
mux.HandleFunc("/first", func(w http.ResponseWriter, r *http.Request) {
13641388
w.Header().Set(headerRateLimit, "60")
13651389
w.Header().Set(headerRateRemaining, "0")
1390+
w.Header().Set(headerRateUsed, "60")
13661391
w.Header().Set(headerRateReset, fmt.Sprint(reset.Unix()))
1392+
w.Header().Set(headerRateResource, "core")
13671393
w.Header().Set("Content-Type", "application/json; charset=utf-8")
13681394
w.WriteHeader(http.StatusForbidden)
13691395
fmt.Fprintln(w, `{
@@ -1406,9 +1432,15 @@ func TestDo_rateLimit_noNetworkCall(t *testing.T) {
14061432
if got, want := rateLimitErr.Rate.Remaining, 0; got != want {
14071433
t.Errorf("rateLimitErr rate remaining = %v, want %v", got, want)
14081434
}
1435+
if got, want := rateLimitErr.Rate.Used, 60; got != want {
1436+
t.Errorf("rateLimitErr rate used = %v, want %v", got, want)
1437+
}
14091438
if !rateLimitErr.Rate.Reset.UTC().Equal(reset) {
14101439
t.Errorf("rateLimitErr rate reset = %v, want %v", rateLimitErr.Rate.Reset.UTC(), reset)
14111440
}
1441+
if got, want := rateLimitErr.Rate.Resource, "core"; got != want {
1442+
t.Errorf("rateLimitErr rate resource = %v, want %v", got, want)
1443+
}
14121444
}
14131445

14141446
// Ignore rate limit headers if the response was served from cache.
@@ -1423,7 +1455,9 @@ func TestDo_rateLimit_ignoredFromCache(t *testing.T) {
14231455
w.Header().Set("X-From-Cache", "1")
14241456
w.Header().Set(headerRateLimit, "60")
14251457
w.Header().Set(headerRateRemaining, "0")
1458+
w.Header().Set(headerRateUsed, "60")
14261459
w.Header().Set(headerRateReset, fmt.Sprint(reset.Unix()))
1460+
w.Header().Set(headerRateResource, "core")
14271461
w.Header().Set("Content-Type", "application/json; charset=utf-8")
14281462
w.WriteHeader(http.StatusForbidden)
14291463
fmt.Fprintln(w, `{
@@ -1470,7 +1504,9 @@ func TestDo_rateLimit_sleepUntilResponseResetLimit(t *testing.T) {
14701504
firstRequest = false
14711505
w.Header().Set(headerRateLimit, "60")
14721506
w.Header().Set(headerRateRemaining, "0")
1507+
w.Header().Set(headerRateUsed, "60")
14731508
w.Header().Set(headerRateReset, fmt.Sprint(reset.Unix()))
1509+
w.Header().Set(headerRateResource, "core")
14741510
w.Header().Set("Content-Type", "application/json; charset=utf-8")
14751511
w.WriteHeader(http.StatusForbidden)
14761512
fmt.Fprintln(w, `{
@@ -1481,7 +1517,9 @@ func TestDo_rateLimit_sleepUntilResponseResetLimit(t *testing.T) {
14811517
}
14821518
w.Header().Set(headerRateLimit, "5000")
14831519
w.Header().Set(headerRateRemaining, "5000")
1520+
w.Header().Set(headerRateUsed, "0")
14841521
w.Header().Set(headerRateReset, fmt.Sprint(reset.Add(time.Hour).Unix()))
1522+
w.Header().Set(headerRateResource, "core")
14851523
w.Header().Set("Content-Type", "application/json; charset=utf-8")
14861524
w.WriteHeader(http.StatusOK)
14871525
fmt.Fprintln(w, `{}`)
@@ -1510,7 +1548,9 @@ func TestDo_rateLimit_sleepUntilResponseResetLimitRetryOnce(t *testing.T) {
15101548
requestCount++
15111549
w.Header().Set(headerRateLimit, "60")
15121550
w.Header().Set(headerRateRemaining, "0")
1551+
w.Header().Set(headerRateUsed, "60")
15131552
w.Header().Set(headerRateReset, fmt.Sprint(reset.Unix()))
1553+
w.Header().Set(headerRateResource, "core")
15141554
w.Header().Set("Content-Type", "application/json; charset=utf-8")
15151555
w.WriteHeader(http.StatusForbidden)
15161556
fmt.Fprintln(w, `{
@@ -1542,7 +1582,9 @@ func TestDo_rateLimit_sleepUntilClientResetLimit(t *testing.T) {
15421582
requestCount++
15431583
w.Header().Set(headerRateLimit, "5000")
15441584
w.Header().Set(headerRateRemaining, "5000")
1585+
w.Header().Set(headerRateUsed, "0")
15451586
w.Header().Set(headerRateReset, fmt.Sprint(reset.Add(time.Hour).Unix()))
1587+
w.Header().Set(headerRateResource, "core")
15461588
w.Header().Set("Content-Type", "application/json; charset=utf-8")
15471589
w.WriteHeader(http.StatusOK)
15481590
fmt.Fprintln(w, `{}`)
@@ -1573,7 +1615,9 @@ func TestDo_rateLimit_abortSleepContextCancelled(t *testing.T) {
15731615
requestCount++
15741616
w.Header().Set(headerRateLimit, "60")
15751617
w.Header().Set(headerRateRemaining, "0")
1618+
w.Header().Set(headerRateUsed, "60")
15761619
w.Header().Set(headerRateReset, fmt.Sprint(reset.Unix()))
1620+
w.Header().Set(headerRateResource, "core")
15771621
w.Header().Set("Content-Type", "application/json; charset=utf-8")
15781622
w.WriteHeader(http.StatusForbidden)
15791623
fmt.Fprintln(w, `{
@@ -1606,7 +1650,9 @@ func TestDo_rateLimit_abortSleepContextCancelledClientLimit(t *testing.T) {
16061650
requestCount++
16071651
w.Header().Set(headerRateLimit, "5000")
16081652
w.Header().Set(headerRateRemaining, "5000")
1653+
w.Header().Set(headerRateUsed, "0")
16091654
w.Header().Set(headerRateReset, fmt.Sprint(reset.Add(time.Hour).Unix()))
1655+
w.Header().Set(headerRateResource, "core")
16101656
w.Header().Set("Content-Type", "application/json; charset=utf-8")
16111657
w.WriteHeader(http.StatusOK)
16121658
fmt.Fprintln(w, `{}`)
@@ -1926,7 +1972,9 @@ func TestCheckResponse_RateLimit(t *testing.T) {
19261972
}
19271973
res.Header.Set(headerRateLimit, "60")
19281974
res.Header.Set(headerRateRemaining, "0")
1975+
res.Header.Set(headerRateUsed, "1")
19291976
res.Header.Set(headerRateReset, "243424")
1977+
res.Header.Set(headerRateResource, "core")
19301978

19311979
err := CheckResponse(res).(*RateLimitError)
19321980

github/rate_limit.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,22 @@ type RateLimitService service
1212

1313
// Rate represents the rate limit for the current client.
1414
type Rate struct {
15-
// The number of requests per hour the client is currently limited to.
15+
// The maximum number of requests that you can make per hour.
1616
Limit int `json:"limit"`
1717

18-
// The number of remaining requests the client can make this hour.
18+
// The number of requests remaining in the current rate limit window.
1919
Remaining int `json:"remaining"`
2020

21-
// The time at which the current rate limit will reset.
21+
// The number of requests you have made in the current rate limit window.
22+
Used int `json:"used"`
23+
24+
// The time at which the current rate limit window resets, in UTC epoch seconds.
2225
Reset Timestamp `json:"reset"`
26+
27+
// The rate limit resource that the request counted against.
28+
// For more information about the different resources, see REST API endpoints for rate limits.
29+
// GitHub API docs: https://docs.github.com/en/rest/rate-limit/rate-limit#get-rate-limit-status-for-the-authenticated-user
30+
Resource string `json:"resource,omitempty"`
2331
}
2432

2533
func (r Rate) String() string {

0 commit comments

Comments
 (0)