fix(GODT-2514): Apply Retry-After to 503 status

Apply the same retry-after code for 429 replies request to 503 replies.
This commit is contained in:
Leander Beernaert
2023-03-27 09:33:45 +02:00
committed by LBeernaertProton
parent 71c20587e0
commit 2751384cef
5 changed files with 60 additions and 11 deletions

View File

@@ -102,6 +102,41 @@ func TestHandleTooManyRequests(t *testing.T) {
}
}
func TestHandleTooManyRequests503(t *testing.T) {
// Create a server with a rate limit of 1 request per second.
s := server.New(server.WithRateLimitAndCustomStatusCode(1, time.Second, http.StatusServiceUnavailable))
defer s.Close()
var calls []server.Call
// Watch the calls made.
s.AddCallWatcher(func(call server.Call) {
calls = append(calls, call)
})
m := proton.New(
proton.WithHostURL(s.GetHostURL()),
proton.WithTransport(proton.InsecureTransport()),
)
defer m.Close()
// Make five calls; they should all succeed, but will be rate limited.
for i := 0; i < 5; i++ {
require.NoError(t, m.Ping(context.Background()))
}
// After each 503 response, we should wait at least the requested duration before making the next request.
for idx, call := range calls {
if call.Status == http.StatusServiceUnavailable {
after, err := strconv.Atoi(call.ResponseHeader.Get("Retry-After"))
require.NoError(t, err)
// The next call should be made after the requested duration.
require.True(t, calls[idx+1].Time.After(call.Time.Add(time.Duration(after)*time.Second)))
}
}
}
func TestHandleTooManyRequests_Malformed(t *testing.T) {
var calls []time.Time

View File

@@ -114,7 +114,7 @@ func updateTime(_ *resty.Client, res *resty.Response) error {
// nolint:gosec
func catchRetryAfter(_ *resty.Client, res *resty.Response) (time.Duration, error) {
// 0 and no error means default behaviour which is exponential backoff with jitter.
if res.StatusCode() != http.StatusTooManyRequests {
if res.StatusCode() != http.StatusTooManyRequests && res.StatusCode() != http.StatusServiceUnavailable {
return 0, nil
}
@@ -139,7 +139,7 @@ func catchRetryAfter(_ *resty.Client, res *resty.Response) (time.Duration, error
}
func catchTooManyRequests(res *resty.Response, _ error) bool {
return res.StatusCode() == http.StatusTooManyRequests
return res.StatusCode() == http.StatusTooManyRequests || res.StatusCode() == http.StatusServiceUnavailable
}
func catchDialError(res *resty.Response, err error) bool {

View File

@@ -22,12 +22,16 @@ type rateLimiter struct {
// countLock is a mutex for the callCount.
countLock sync.Mutex
// statusCode to reply with
statusCode int
}
func newRateLimiter(limit int, window time.Duration) *rateLimiter {
func newRateLimiter(limit int, window time.Duration, statusCode int) *rateLimiter {
return &rateLimiter{
limit: limit,
window: window,
limit: limit,
window: window,
statusCode: statusCode,
}
}

View File

@@ -202,7 +202,7 @@ func (s *Server) applyRateLimit() gin.HandlerFunc {
if wait := s.rateLimit.exceeded(); wait > 0 {
c.Header("Retry-After", strconv.Itoa(int(wait.Seconds())))
c.AbortWithStatus(http.StatusTooManyRequests)
c.AbortWithStatus(s.rateLimit.statusCode)
}
}
}

View File

@@ -186,18 +186,28 @@ func (opt withAuthCache) config(builder *serverBuilder) {
func WithRateLimit(limit int, window time.Duration) Option {
return &withRateLimit{
limit: limit,
window: window,
limit: limit,
window: window,
statusCode: http.StatusTooManyRequests,
}
}
func WithRateLimitAndCustomStatusCode(limit int, window time.Duration, code int) Option {
return &withRateLimit{
limit: limit,
window: window,
statusCode: code,
}
}
type withRateLimit struct {
limit int
window time.Duration
limit int
statusCode int
window time.Duration
}
func (opt withRateLimit) config(builder *serverBuilder) {
builder.rateLimiter = newRateLimiter(opt.limit, opt.window)
builder.rateLimiter = newRateLimiter(opt.limit, opt.window, opt.statusCode)
}
func WithProxyTransport(transport *http.Transport) Option {