-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add RateLimitError type, detect and return it when appropriate. #277
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -236,7 +236,7 @@ type Response struct { | |
func newResponse(r *http.Response) *Response { | ||
response := &Response{Response: r} | ||
response.populatePageValues() | ||
response.populateRate() | ||
response.Rate = parseRate(r) | ||
return response | ||
} | ||
|
||
|
@@ -284,19 +284,21 @@ func (r *Response) populatePageValues() { | |
} | ||
} | ||
|
||
// populateRate parses the rate related headers and populates the response Rate. | ||
func (r *Response) populateRate() { | ||
// parseRate parses the rate related headers. | ||
func parseRate(r *http.Response) Rate { | ||
var rate Rate | ||
if limit := r.Header.Get(headerRateLimit); limit != "" { | ||
r.Rate.Limit, _ = strconv.Atoi(limit) | ||
rate.Limit, _ = strconv.Atoi(limit) | ||
} | ||
if remaining := r.Header.Get(headerRateRemaining); remaining != "" { | ||
r.Rate.Remaining, _ = strconv.Atoi(remaining) | ||
rate.Remaining, _ = strconv.Atoi(remaining) | ||
} | ||
if reset := r.Header.Get(headerRateReset); reset != "" { | ||
if v, _ := strconv.ParseInt(reset, 10, 64); v != 0 { | ||
r.Rate.Reset = Timestamp{time.Unix(v, 0)} | ||
rate.Reset = Timestamp{time.Unix(v, 0)} | ||
} | ||
} | ||
return rate | ||
} | ||
|
||
// Rate specifies the current rate limit for the client as determined by the | ||
|
@@ -373,6 +375,20 @@ type TwoFactorAuthError ErrorResponse | |
|
||
func (r *TwoFactorAuthError) Error() string { return (*ErrorResponse)(r).Error() } | ||
|
||
// RateLimitError occurs when GitHub returns 403 Forbidden response with a rate limit | ||
// remaining value of 0, and error message starts with "API rate limit exceeded for ". | ||
type RateLimitError struct { | ||
Rate Rate // Rate specifies last known rate limit for the client | ||
Response *http.Response // HTTP response that caused this error | ||
Message string `json:"message"` // error message | ||
} | ||
|
||
func (r *RateLimitError) Error() string { | ||
return fmt.Sprintf("%v %v: %d %v; rate reset in %v", | ||
r.Response.Request.Method, sanitizeURL(r.Response.Request.URL), | ||
r.Response.StatusCode, r.Message, r.Rate.Reset.Time.Sub(time.Now())) | ||
} | ||
|
||
// sanitizeURL redacts the client_secret parameter from the URL which may be | ||
// exposed to the user, specifically in the ErrorResponse error message. | ||
func sanitizeURL(uri *url.URL) *url.URL { | ||
|
@@ -427,10 +443,18 @@ func CheckResponse(r *http.Response) error { | |
if err == nil && data != nil { | ||
json.Unmarshal(data, errorResponse) | ||
} | ||
if r.StatusCode == http.StatusUnauthorized && strings.HasPrefix(r.Header.Get(headerOTP), "required") { | ||
switch { | ||
case r.StatusCode == http.StatusUnauthorized && strings.HasPrefix(r.Header.Get(headerOTP), "required"): | ||
return (*TwoFactorAuthError)(errorResponse) | ||
case r.StatusCode == http.StatusForbidden && r.Header.Get(headerRateRemaining) == "0" && strings.HasPrefix(errorResponse.Message, "API rate limit exceeded for "): | ||
return &RateLimitError{ | ||
Rate: parseRate(r), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Technically, it's possible to avoid having to parse rate from response := newResponse(resp) // response.Rate is parsed and available here.
c.rateMu.Lock()
c.rate = response.Rate
c.rateMu.Unlock()
err = CheckResponse(resp) // CheckResponse is not given the Rate, so it has to parse it from resp a second time... The problem is that However, it might be possible to create an unexported There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm actually fine either way. It's unlikely the extra call will have a measurable difference (though I guess we could benchmark if we really wanted to be sure :) ) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll keep it as is, because |
||
Response: errorResponse.Response, | ||
Message: errorResponse.Message, | ||
} | ||
default: | ||
return errorResponse | ||
} | ||
return errorResponse | ||
} | ||
|
||
// parseBoolResponse determines the boolean result from a GitHub API response. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it's worth expanding documentation of
CheckError
to mention bothTwoFactorAuthError
andRateLimitError
error types?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, probably so.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 76866e1. PTAL.