diff --git a/rest_framework/views.py b/rest_framework/views.py index 832f17233..5f52adaa5 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -356,7 +356,17 @@ class APIView(View): throttle_durations.append(throttle.wait()) if throttle_durations: - self.throttled(request, max(throttle_durations)) + # Filter out `None` values which may happen in case of config / rate + # changes, see #1438 + durations = [ + duration for duration in throttle_durations + if duration is not None + ] + + if durations: + self.throttled(request, max(durations)) + else: + self.throttled(request, None) def determine_version(self, request, *args, **kwargs): """ diff --git a/tests/test_throttling.py b/tests/test_throttling.py index 3c172e263..2cc60a1a7 100644 --- a/tests/test_throttling.py +++ b/tests/test_throttling.py @@ -159,6 +159,26 @@ class ThrottlingTests(TestCase): assert response.status_code == 429 assert int(response['retry-after']) == 58 + def test_handle_negative_throttle_value(self): + self.set_throttle_timer(MockView_DoubleThrottling, 0) + request = self.factory.get('/') + for dummy in range(24): + response = MockView_DoubleThrottling.as_view()(request) + assert response.status_code == 429 + assert int(response['retry-after']) == 60 + + previous_rate = User3SecRateThrottle.rate + User3SecRateThrottle.rate = '1/sec' + + for dummy in range(24): + response = MockView_DoubleThrottling.as_view()(request) + + assert response.status_code == 429 + assert int(response['retry-after']) == 60 + + # reset + User3SecRateThrottle.rate = previous_rate + def ensure_response_header_contains_proper_throttle_field(self, view, expected_headers): """ Ensure the response returns an Retry-After field with status and next attributes