- Applied to TorBox, can be extended easily to other providers - Reads response headers commonplace for pre-emptive rate limit throttling and retry-after - When a rate limit is reached, displays a warning in the UI - Fix socket leak from direct allocation of HttpClient, replaced with factory which handles pooling and re-use of sockets. While HttpClient is Disposable, it doesn't gaurantee (and does not) directly release underlying sockets for queries at the time the client is disposed. These sockets will go into a TCP WAIT state often for a very long time. The expected pattern in C# is to always use the HttClientFactory which will correctly handle re-use of the OS sockets in suqsequent queries reducing resource and memory leaks.
52 lines
1.8 KiB
C#
52 lines
1.8 KiB
C#
using System.Net;
|
|
using RdtClient.Data.Models.Internal;
|
|
using RdtClient.Service.Helpers;
|
|
|
|
namespace RdtClient.Service.Test.Helpers;
|
|
|
|
public class RateLimitHandlerTest
|
|
{
|
|
[Fact]
|
|
public async Task SendAsync_ThrowsRateLimitException_On429WithRetryAfter()
|
|
{
|
|
// Arrange
|
|
var handler = new RateLimitHandler
|
|
{
|
|
InnerHandler = new MockHttpMessageHandler(HttpStatusCode.TooManyRequests, 3600)
|
|
};
|
|
var client = new HttpClient(handler);
|
|
|
|
// Act & Assert
|
|
var ex = await Assert.ThrowsAsync<RateLimitException>(() => client.GetAsync("http://example.com"));
|
|
Assert.Equal(TimeSpan.FromSeconds(3600), ex.RetryAfter);
|
|
Assert.Equal("TorBox rate limit exceeded", ex.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendAsync_ThrowsRateLimitException_On429WithoutRetryAfter()
|
|
{
|
|
// Arrange
|
|
var handler = new RateLimitHandler
|
|
{
|
|
InnerHandler = new MockHttpMessageHandler(HttpStatusCode.TooManyRequests, null)
|
|
};
|
|
var client = new HttpClient(handler);
|
|
|
|
// Act & Assert
|
|
var ex = await Assert.ThrowsAsync<RateLimitException>(() => client.GetAsync("http://example.com"));
|
|
Assert.Equal(TimeSpan.FromMinutes(2), ex.RetryAfter);
|
|
}
|
|
|
|
private class MockHttpMessageHandler(HttpStatusCode statusCode, Int32? retryAfterSeconds) : HttpMessageHandler
|
|
{
|
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
|
{
|
|
var response = new HttpResponseMessage(statusCode);
|
|
if (retryAfterSeconds.HasValue)
|
|
{
|
|
response.Headers.RetryAfter = new System.Net.Http.Headers.RetryConditionHeaderValue(TimeSpan.FromSeconds(retryAfterSeconds.Value));
|
|
}
|
|
return Task.FromResult(response);
|
|
}
|
|
}
|
|
}
|