Browse Source

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 #431.
Dmitri Shuralyov 9 years ago
parent
commit
59307ef6be
2 changed files with 97 additions and 0 deletions
  1. +32
    -0
      github/github.go
  2. +65
    -0
      github/github_test.go

+ 32
- 0
github/github.go View File

@ -494,6 +494,24 @@ func (r *RateLimitError) Error() string {
r.Response.StatusCode, r.Message, r.Rate.Reset.Time.Sub(time.Now())) r.Response.StatusCode, r.Message, r.Rate.Reset.Time.Sub(time.Now()))
} }
// AbuseRateLimitError occurs when GitHub returns 403 Forbidden response with the
// "documentation_url" field value equal to "https://developer.github.com/v3#abuse-rate-limits".
type AbuseRateLimitError struct {
Response *http.Response // HTTP response that caused this error
Message string `json:"message"` // error message
// RetryAfter is provided with some abuse rate limit errors. If present,
// it is the amount of time that the client should wait before retrying.
// Otherwise, the client should try again later (after an unspecified amount of time).
RetryAfter *time.Duration
}
func (r *AbuseRateLimitError) Error() string {
return fmt.Sprintf("%v %v: %d %v",
r.Response.Request.Method, sanitizeURL(r.Response.Request.URL),
r.Response.StatusCode, r.Message)
}
// sanitizeURL redacts the client_secret parameter from the URL which may be // sanitizeURL redacts the client_secret parameter from the URL which may be
// exposed to the user, specifically in the ErrorResponse error message. // exposed to the user, specifically in the ErrorResponse error message.
func sanitizeURL(uri *url.URL) *url.URL { func sanitizeURL(uri *url.URL) *url.URL {
@ -564,6 +582,20 @@ func CheckResponse(r *http.Response) error {
Response: errorResponse.Response, Response: errorResponse.Response,
Message: errorResponse.Message, Message: errorResponse.Message,
} }
case r.StatusCode == http.StatusForbidden && errorResponse.DocumentationURL == "https://developer.github.com/v3#abuse-rate-limits":
abuseRateLimitError := &AbuseRateLimitError{
Response: errorResponse.Response,
Message: errorResponse.Message,
}
if v := r.Header["Retry-After"]; len(v) > 0 {
// According to GitHub support, the "Retry-After" header value will be
// an integer which represents the number of seconds that one should
// wait before resuming making requests.
retryAfterSeconds, _ := strconv.ParseInt(v[0], 10, 64) // Error handling is noop.
retryAfter := time.Duration(retryAfterSeconds) * time.Second
abuseRateLimitError.RetryAfter = &retryAfter
}
return abuseRateLimitError
default: default:
return errorResponse return errorResponse
} }


+ 65
- 0
github/github_test.go View File

@ -538,6 +538,71 @@ func TestDo_rateLimit_noNetworkCall(t *testing.T) {
} }
} }
// Ensure *AbuseRateLimitError is returned when the response indicates that
// the client has triggered an abuse detection mechanism.
func TestDo_rateLimit_abuseRateLimitError(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
// When the abuse rate limit error is of the "temporarily blocked from content creation" type,
// there is no "Retry-After" header.
fmt.Fprintln(w, `{
"message": "You have triggered an abuse detection mechanism and have been temporarily blocked from content creation. Please retry your request again later.",
"documentation_url": "https://developer.github.com/v3#abuse-rate-limits"
}`)
})
req, _ := client.NewRequest("GET", "/", nil)
_, err := client.Do(req, nil)
if err == nil {
t.Error("Expected error to be returned.")
}
abuseRateLimitErr, ok := err.(*AbuseRateLimitError)
if !ok {
t.Fatalf("Expected a *AbuseRateLimitError error; got %#v.", err)
}
if got, want := abuseRateLimitErr.RetryAfter, (*time.Duration)(nil); got != want {
t.Errorf("abuseRateLimitErr RetryAfter = %v, want %v", got, want)
}
}
// Ensure *AbuseRateLimitError.RetryAfter is parsed correctly.
func TestDo_rateLimit_abuseRateLimitError_retryAfter(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Retry-After", "123") // Retry after value of 123 seconds.
w.WriteHeader(http.StatusForbidden)
fmt.Fprintln(w, `{
"message": "You have triggered an abuse detection mechanism ...",
"documentation_url": "https://developer.github.com/v3#abuse-rate-limits"
}`)
})
req, _ := client.NewRequest("GET", "/", nil)
_, err := client.Do(req, nil)
if err == nil {
t.Error("Expected error to be returned.")
}
abuseRateLimitErr, ok := err.(*AbuseRateLimitError)
if !ok {
t.Fatalf("Expected a *AbuseRateLimitError error; got %#v.", err)
}
if abuseRateLimitErr.RetryAfter == nil {
t.Fatalf("abuseRateLimitErr RetryAfter is nil, expected not-nil")
}
if got, want := *abuseRateLimitErr.RetryAfter, 123*time.Second; got != want {
t.Errorf("abuseRateLimitErr RetryAfter = %v, want %v", got, want)
}
}
func TestDo_noContent(t *testing.T) { func TestDo_noContent(t *testing.T) {
setup() setup()
defer teardown() defer teardown()


Loading…
Cancel
Save