Repository: getsentry/sentry

Issue:
Retry-After header is a floating point
According to the [RFC 7231](https://tools.ietf.org/html/rfc7231#section-7.1.3), Retry-After should be either date or seconds where seconds are:

>  A delay-seconds value is a non-negative decimal integer, representing time in seconds.

[Sentry on the other hand is returning](https://github.com/getsentry/sentry/blob/15e96f75356182527f203586023b52dd2b4ceae2/src/sentry/quotas/redis.py#L199) a floating point number: seconds.milliseconds
For example just under 5 minutes: 

`292.052427053`

The issue it causes with regards to .NET is that the [HttpResponseHeaders.RetryAfter](https://github.com/dotnet/corefx/blob/master/src/System.Net.Http/src/System/Net/Http/Headers/RetryConditionHeaderValue.cs#L151) is not able to parse the value.

It can be worked around by reading the header directly and parsing with double:

```csharp
if (response.Headers.TryGetValues("Retry-After", out var values)
            && double.TryParse(values?.FirstOrDefault(), out var retryAfterSeconds))
{
    var retryAfterSpan = TimeSpan.FromSeconds(retryAfterSeconds);
}
```
Ideally though, one would use the framework's constructs, like:

```csharp
if (response.Headers?.RetryAfter?.Delta != null)
{
     response.Headers.RetryAfter.Delta.Value
}
```

Sentry version tested was 8.22


Discussion:
Should probably cast to int here: https://github.com/getsentry/sentry/blob/dd50f1fdb70297b9c9fccd31cf51e26d5601e144/src/sentry/web/api.py#L137

Resolving patch:
diff --git a/src/sentry/web/api.py b/src/sentry/web/api.py
--- a/src/sentry/web/api.py
+++ b/src/sentry/web/api.py
@@ -1,6 +1,8 @@
 from __future__ import absolute_import, print_function
 
 import base64
+import math
+
 import jsonschema
 import logging
 import os
@@ -136,7 +138,7 @@ def dispatch(self, request, project_id=None, *args, **kwargs):
             response['X-Sentry-Error'] = context['error']
 
             if isinstance(e, APIRateLimited) and e.retry_after is not None:
-                response['Retry-After'] = six.text_type(e.retry_after)
+                response['Retry-After'] = six.text_type(int(math.ceil(e.retry_after)))
 
         except Exception as e:
             # TODO(dcramer): test failures are not outputting the log message


Test patch:
diff --git a/tests/sentry/web/api/tests.py b/tests/sentry/web/api/tests.py
--- a/tests/sentry/web/api/tests.py
+++ b/tests/sentry/web/api/tests.py
@@ -8,6 +8,7 @@
 from exam import fixture
 from mock import Mock
 
+from sentry.coreapi import APIRateLimited
 from sentry.models import ProjectKey
 from sentry.signals import event_accepted, event_dropped, event_filtered
 from sentry.testutils import (assert_mock_called_once_with_partial, TestCase)
@@ -746,3 +747,14 @@ def test_robots(self):
         resp = self.client.get(self.path)
         assert resp.status_code == 200
         assert resp['Content-Type'] == 'text/plain'
+
+
+def rate_limited_dispatch(*args, **kwargs):
+    raise APIRateLimited(retry_after=42.42)
+
+
+class APIViewTest(TestCase):
+    @mock.patch('sentry.web.api.APIView._dispatch', new=rate_limited_dispatch)
+    def test_retry_after_int(self):
+        resp = self._postWithHeader({})
+        assert resp['Retry-After'] == '43'