Smarter rate limit handling

pull/980/head
Tyrrrz 2 years ago
parent 57e2dc9313
commit 49c4b12512

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
@ -35,7 +36,7 @@ public class DiscordClient
{
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(_baseUri, url));
// Don't validate because token can have invalid characters
// Don't validate because token can have special characters
// https://github.com/Tyrrrz/DiscordChatExporter/issues/828
request.Headers.TryAddWithoutValidation(
"Authorization",
@ -44,11 +45,32 @@ public class DiscordClient
: _token
);
return await Http.Client.SendAsync(
var response = await Http.Client.SendAsync(
request,
HttpCompletionOption.ResponseHeadersRead,
innerCancellationToken
);
// If this was the last request available before hitting the rate limit,
// wait out the reset time so that future requests can succeed.
// This may add an unnecessary delay in case the user doesn't intend to
// make any more requests, but implementing a smarter solution would
// require properly keeping track of Discord's global/per-route/per-resource
// rate limits and that's not worth the effort.
var remainingRequestCount = response
.Headers
.TryGetValue("X-RateLimit-Remaining")?
.Pipe(s => int.Parse(s, CultureInfo.InvariantCulture));
var resetAfterDelay = response
.Headers
.TryGetValue("X-RateLimit-Reset-After")?
.Pipe(s => TimeSpan.FromSeconds(double.Parse(s, CultureInfo.InvariantCulture)));
if (remainingRequestCount <= 0 && resetAfterDelay is not null)
await Task.Delay(resetAfterDelay.Value + TimeSpan.FromSeconds(1), innerCancellationToken);
return response;
}, cancellationToken);
}

@ -4,7 +4,7 @@ namespace DiscordChatExporter.Core.Utils.Extensions;
public static class HttpExtensions
{
public static string? TryGetValue(this HttpContentHeaders headers, string name) =>
public static string? TryGetValue(this HttpHeaders headers, string name) =>
headers.TryGetValues(name, out var values)
? string.Concat(values)
: null;

@ -39,19 +39,9 @@ public static class Http
8,
(i, result, _) =>
{
// If rate-limited, use retry-after as a guide
if (result.Result?.StatusCode == HttpStatusCode.TooManyRequests)
{
// Only start respecting retry-after after a few attempts, because
// Discord often sends unreasonable (20+ minutes) retry-after
// on the very first request.
if (i > 3)
{
var retryAfterDelay = result.Result.Headers.RetryAfter?.Delta;
if (retryAfterDelay is not null)
return retryAfterDelay.Value + TimeSpan.FromSeconds(1); // margin just in case
}
}
// If rate-limited, use retry-after header as the guide
if (result.Result.Headers.RetryAfter?.Delta is { } retryAfter)
return retryAfter + TimeSpan.FromSeconds(1);
return TimeSpan.FromSeconds(Math.Pow(2, i) + 1);
},

Loading…
Cancel
Save