diff --git a/ASFFreeGames/Reddit/RedditHelper.cs b/ASFFreeGames/Reddit/RedditHelper.cs
index 0f8d0e8..1bbfc3f 100644
--- a/ASFFreeGames/Reddit/RedditHelper.cs
+++ b/ASFFreeGames/Reddit/RedditHelper.cs
@@ -157,39 +157,45 @@ internal static RedditGameEntry[] LoadMessages(JsonNode children) {
/// Thrown when Reddit returns a server error.
/// This method is based on this GitHub issue: https://github.com/maxisoft/ASFFreeGames/issues/28
private static async ValueTask GetPayload(SimpleHttpClient httpClient, CancellationToken cancellationToken, uint retry = 5) {
- HttpStreamResponse? stream = null;
- var headers = new Dictionary();
- headers.Add("Pragma", "no-cache");
- headers.Add("Cache-Control", "no-cache");
- headers.Add("Accept", "application/json");
- headers.Add("Sec-Fetch-Site", "none");
- headers.Add("Sec-Fetch-Mode", "no-cors");
- headers.Add("Sec-Fetch-Dest", "empty");
+ HttpStreamResponse? response = null;
+
+ Dictionary headers = new() {
+ { "Pragma", "no-cache" },
+ { "Cache-Control", "no-cache" },
+ { "Accept", "application/json" },
+ { "Sec-Fetch-Site", "none" },
+ { "Sec-Fetch-Mode", "no-cors" },
+ { "Sec-Fetch-Dest", "empty" }
+ };
for (int t = 0; t < retry; t++) {
try {
#pragma warning disable CA2000
- stream = await httpClient.GetStreamAsync(GetUrl(), headers, cancellationToken).ConfigureAwait(false);
+ response = await httpClient.GetStreamAsync(GetUrl(), headers, cancellationToken).ConfigureAwait(false);
#pragma warning restore CA2000
- if (!stream.StatusCode.IsSuccessCode()) {
- throw new RedditServerException($"reddit http error code is {stream.StatusCode}", stream.StatusCode);
+ if (await HandleTooManyRequest(response, cancellationToken: cancellationToken).ConfigureAwait(false)) {
+ continue;
}
- JsonNode? res = await ParseJsonNode(stream, cancellationToken).ConfigureAwait(false);
+ if (!response.StatusCode.IsSuccessCode()) {
+ throw new RedditServerException($"reddit http error code is {response.StatusCode}", response.StatusCode);
+ }
+
+ JsonNode? res = await ParseJsonNode(response, cancellationToken).ConfigureAwait(false);
if (res is null) {
- throw new RedditServerException("empty response", stream.StatusCode);
+ throw new RedditServerException("empty response", response.StatusCode);
}
try {
if ((res["kind"]?.GetValue() != "Listing") ||
res["data"] is null) {
- throw new RedditServerException("invalid response", stream.StatusCode);
+ throw new RedditServerException("invalid response", response.StatusCode);
}
}
catch (Exception e) when (e is FormatException or InvalidOperationException) {
- throw new RedditServerException("invalid response", stream.StatusCode);
+ throw new RedditServerException("invalid response", response.StatusCode);
}
return res;
@@ -203,11 +209,11 @@ private static async ValueTask GetPayload(SimpleHttpClient httpClient,
cancellationToken.ThrowIfCancellationRequested();
}
finally {
- if (stream is not null) {
- await stream.DisposeAsync().ConfigureAwait(false);
+ if (response is not null) {
+ await response.DisposeAsync().ConfigureAwait(false);
}
- stream = null;
+ response = null;
}
await Task.Delay((2 << (t + 1)) * 100, cancellationToken).ConfigureAwait(false);
@@ -217,6 +223,44 @@ private static async ValueTask GetPayload(SimpleHttpClient httpClient,
return JsonNode.Parse("{}")!;
}
+ ///
+ /// Handles too many requests by checking the status code and headers of the response.
+ /// If the status code is Forbidden or TooManyRequests, it checks the remaining rate limit
+ /// and the reset time. If the remaining rate limit is less than or equal to 0, it delays
+ /// the execution until the reset time using the cancellation token.
+ ///
+ /// The HTTP stream response to handle.
+ ///
+ /// The cancellation token.
+ /// True if the request was handled & awaited, false otherwise.
+ private static async ValueTask HandleTooManyRequest(HttpStreamResponse response, int maxTimeToWait = 60, CancellationToken cancellationToken = default) {
+ if (response.StatusCode is HttpStatusCode.Forbidden or HttpStatusCode.TooManyRequests) {
+ if (response.Response.Headers.TryGetValues("x-ratelimit-remaining", out IEnumerable? rateLimitRemaining)) {
+ if (int.TryParse(rateLimitRemaining.FirstOrDefault(), out int remaining) && (remaining <= 0)) {
+ if (response.Response.Headers.TryGetValues("x-ratelimit-reset", out IEnumerable? rateLimitReset)
+ && float.TryParse(rateLimitReset.FirstOrDefault(), out float reset) && double.IsNormal(reset) && (0 < reset) && (reset < maxTimeToWait)) {
+ try {
+ await Task.Delay(TimeSpan.FromSeconds(reset), cancellationToken).ConfigureAwait(false);
+ }
+ catch (TaskCanceledException) {
+ return false;
+ }
+ catch (TimeoutException) {
+ return false;
+ }
+ catch (OperationCanceledException) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
///
/// Parses a JSON object from a stream response. Using not straightforward for ASF trimmed compatibility reasons
///