-
-
Notifications
You must be signed in to change notification settings - Fork 234
Expand file tree
/
Copy pathRetryAfterHandler.cs
More file actions
100 lines (84 loc) · 3.78 KB
/
RetryAfterHandler.cs
File metadata and controls
100 lines (84 loc) · 3.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
using Sentry.Infrastructure;
namespace Sentry.Internal.Http;
/// <summary>
/// Retry After Handler which short-circuit requests following an HTTP 429.
/// </summary>
/// <seealso href="https://tools.ietf.org/html/rfc6585#section-4" />
/// <seealso href="https://develop.sentry.dev/sdk/overview/#writing-an-sdk"/>
/// <inheritdoc />
internal class RetryAfterHandler : DelegatingHandler
{
private readonly ISystemClock _clock;
private const HttpStatusCode TooManyRequests = (HttpStatusCode)429;
internal static readonly TimeSpan DefaultRetryAfterDelay = TimeSpan.FromSeconds(60);
private long _retryAfterUtcTicks;
internal long RetryAfterUtcTicks => _retryAfterUtcTicks;
/// <summary>
/// Initializes a new instance of the <see cref="RetryAfterHandler"/> class.
/// </summary>
/// <param name="innerHandler">The inner handler which is responsible for processing the HTTP response messages.</param>
public RetryAfterHandler(HttpMessageHandler innerHandler)
: this(innerHandler, SystemClock.Clock)
{ }
internal RetryAfterHandler(HttpMessageHandler innerHandler, ISystemClock clock)
: base(innerHandler)
{
ArgumentNullException.ThrowIfNull(clock);
_clock = clock;
}
/// <summary>
/// Sends an HTTP request to the inner handler while verifying the Response status code for HTTP 429.
/// </summary>
/// <param name="request">The HTTP request message to send to the server.</param>
/// <param name="cancellationToken">A cancellation token to cancel operation.</param>
/// <returns>
/// The task object representing the asynchronous operation.
/// </returns>
/// <inheritdoc />
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var retryAfter = Interlocked.CompareExchange(ref _retryAfterUtcTicks, 0, 0);
if (retryAfter != 0)
{
if (retryAfter > _clock.GetUtcNow().Ticks)
{
// Important: don't reuse the same HttpResponseMessage in multiple requests!
// https://github.com/getsentry/sentry-dotnet/issues/800
return new HttpResponseMessage(TooManyRequests);
}
_ = Interlocked.Exchange(ref _retryAfterUtcTicks, 0);
}
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (response.StatusCode == TooManyRequests)
{
var retryAfterTimestamp = GetRetryAfterTimestamp(response);
_ = Interlocked.Exchange(ref _retryAfterUtcTicks, retryAfterTimestamp.UtcTicks);
}
return response;
}
private DateTimeOffset GetRetryAfterTimestamp(HttpResponseMessage response)
{
if (response.Headers.RetryAfter != null)
{
if (response.Headers.RetryAfter.Delta is { } delta)
{
return _clock.GetUtcNow() + delta;
}
if (response.Headers.RetryAfter.Date is { } date)
{
return date;
}
}
// Sentry was sending floating point numbers which are not handled by RetryConditionHeaderValue
// To be compatible with older versions of sentry on premise: https://github.com/getsentry/sentry/issues/7919
if (response.Headers.TryGetValues("Retry-After", out var values)
&& double.TryParse(values.FirstOrDefault(), NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var retryAfterSeconds))
{
return _clock.GetUtcNow().AddTicks((long)(retryAfterSeconds * TimeSpan.TicksPerSecond));
}
// No retry header was sent. Use the default retry delay.
return _clock.GetUtcNow() + DefaultRetryAfterDelay;
}
}