Skip to content

Commit eed6e4c

Browse files
committed
Add and detect AbuseRateLimitError.
It's similar to RateLimitError, but it's a different type of rate limit error. It is documented at: https://developer.github.com/v3/#abuse-rate-limits Parse and include the Retry-After header value, if present. It is documented at: https://developer.github.com/guides/best-practices-for-integrators/#dealing-with-abuse-rate-limits According to GitHub support, its type is an integer value representing seconds. Helps google#431.
1 parent c9db26e commit eed6e4c

File tree

2 files changed

+97
-0
lines changed

2 files changed

+97
-0
lines changed

github/github.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,24 @@ func (r *RateLimitError) Error() string {
494494
r.Response.StatusCode, r.Message, r.Rate.Reset.Time.Sub(time.Now()))
495495
}
496496

497+
// AbuseRateLimitError occurs when GitHub returns 403 Forbidden response with the
498+
// "documentation_url" field value equal to "https://developer.github.com/v3#abuse-rate-limits".
499+
type AbuseRateLimitError struct {
500+
Response *http.Response // HTTP response that caused this error
501+
Message string `json:"message"` // error message
502+
503+
// RetryAfter is provided with some abuse rate limit errors. If present,
504+
// it is the amount of time that the client should wait before retrying.
505+
// Otherwise, the client should try again later (after an unspecified amount of time).
506+
RetryAfter *time.Duration
507+
}
508+
509+
func (r *AbuseRateLimitError) Error() string {
510+
return fmt.Sprintf("%v %v: %d %v",
511+
r.Response.Request.Method, sanitizeURL(r.Response.Request.URL),
512+
r.Response.StatusCode, r.Message)
513+
}
514+
497515
// sanitizeURL redacts the client_secret parameter from the URL which may be
498516
// exposed to the user, specifically in the ErrorResponse error message.
499517
func sanitizeURL(uri *url.URL) *url.URL {
@@ -564,6 +582,20 @@ func CheckResponse(r *http.Response) error {
564582
Response: errorResponse.Response,
565583
Message: errorResponse.Message,
566584
}
585+
case r.StatusCode == http.StatusForbidden && errorResponse.DocumentationURL == "https://developer.github.com/v3#abuse-rate-limits":
586+
abuseRateLimitError := &AbuseRateLimitError{
587+
Response: errorResponse.Response,
588+
Message: errorResponse.Message,
589+
}
590+
if v := r.Header["Retry-After"]; len(v) > 0 {
591+
// According to GitHub support, the "Retry-After" header value will be
592+
// an integer which represents the number of seconds that one should
593+
// wait before resuming making requests.
594+
retryAfterSeconds, _ := strconv.ParseInt(v[0], 10, 64) // Error handling is noop.
595+
retryAfter := time.Duration(retryAfterSeconds) * time.Second
596+
abuseRateLimitError.RetryAfter = &retryAfter
597+
}
598+
return abuseRateLimitError
567599
default:
568600
return errorResponse
569601
}

github/github_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,71 @@ func TestDo_rateLimit_noNetworkCall(t *testing.T) {
538538
}
539539
}
540540

541+
// Ensure *AbuseRateLimitError is returned when the response indicates that
542+
// the client has triggered an abuse detection mechanism.
543+
func TestDo_rateLimit_abuseRateLimitError(t *testing.T) {
544+
setup()
545+
defer teardown()
546+
547+
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
548+
w.Header().Set("Content-Type", "application/json; charset=utf-8")
549+
w.WriteHeader(http.StatusForbidden)
550+
// When the abuse rate limit error is of the "temporarily blocked from content creation" type,
551+
// there is no "Retry-After" header.
552+
fmt.Fprintln(w, `{
553+
"message": "You have triggered an abuse detection mechanism and have been temporarily blocked from content creation. Please retry your request again later.",
554+
"documentation_url": "https://developer.github.com/v3#abuse-rate-limits"
555+
}`)
556+
})
557+
558+
req, _ := client.NewRequest("GET", "/", nil)
559+
_, err := client.Do(req, nil)
560+
561+
if err == nil {
562+
t.Error("Expected error to be returned.")
563+
}
564+
abuseRateLimitErr, ok := err.(*AbuseRateLimitError)
565+
if !ok {
566+
t.Fatalf("Expected a *AbuseRateLimitError error; got %#v.", err)
567+
}
568+
if got, want := abuseRateLimitErr.RetryAfter, (*time.Duration)(nil); got != want {
569+
t.Errorf("abuseRateLimitErr RetryAfter = %v, want %v", got, want)
570+
}
571+
}
572+
573+
// Ensure *AbuseRateLimitError.RetryAfter is parsed correctly.
574+
func TestDo_rateLimit_abuseRateLimitError_retryAfter(t *testing.T) {
575+
setup()
576+
defer teardown()
577+
578+
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
579+
w.Header().Set("Content-Type", "application/json; charset=utf-8")
580+
w.Header().Set("Retry-After", "123") // Retry after value of 123 seconds.
581+
w.WriteHeader(http.StatusForbidden)
582+
fmt.Fprintln(w, `{
583+
"message": "You have triggered an abuse detection mechanism ...",
584+
"documentation_url": "https://developer.github.com/v3#abuse-rate-limits"
585+
}`)
586+
})
587+
588+
req, _ := client.NewRequest("GET", "/", nil)
589+
_, err := client.Do(req, nil)
590+
591+
if err == nil {
592+
t.Error("Expected error to be returned.")
593+
}
594+
abuseRateLimitErr, ok := err.(*AbuseRateLimitError)
595+
if !ok {
596+
t.Fatalf("Expected a *AbuseRateLimitError error; got %#v.", err)
597+
}
598+
if abuseRateLimitErr.RetryAfter == nil {
599+
t.Fatalf("abuseRateLimitErr RetryAfter is nil, expected not-nil")
600+
}
601+
if got, want := *abuseRateLimitErr.RetryAfter, 123*time.Second; got != want {
602+
t.Errorf("abuseRateLimitErr RetryAfter = %v, want %v", got, want)
603+
}
604+
}
605+
541606
func TestDo_noContent(t *testing.T) {
542607
setup()
543608
defer teardown()

0 commit comments

Comments
 (0)