mirror of
https://github.com/JKorf/CryptoExchange.Net
synced 2025-06-07 16:06:15 +00:00
feature: Handle error 429 when connecting websocket (#213)
* feature: Handle error 429 when connecting websocket * Add preprocessor directive for NET6_0_OR_GREATER when checking for connection rate limit
This commit is contained in:
parent
fee18fd183
commit
5d3de52da6
@ -519,6 +519,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
var socket = CreateSocket(connectionAddress.Data!);
|
var socket = CreateSocket(connectionAddress.Data!);
|
||||||
var socketConnection = new SocketConnection(_logger, this, socket, address);
|
var socketConnection = new SocketConnection(_logger, this, socket, address);
|
||||||
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
||||||
|
socketConnection.ConnectRateLimitedAsync += HandleConnectRateLimitedAsync;
|
||||||
socketConnection.DedicatedRequestConnection = dedicatedRequestConnection;
|
socketConnection.DedicatedRequestConnection = dedicatedRequestConnection;
|
||||||
|
|
||||||
foreach (var ptg in PeriodicTaskRegistrations)
|
foreach (var ptg in PeriodicTaskRegistrations)
|
||||||
@ -538,6 +539,19 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process connect rate limited
|
||||||
|
/// </summary>
|
||||||
|
protected async virtual Task HandleConnectRateLimitedAsync()
|
||||||
|
{
|
||||||
|
if (ClientOptions.RateLimiterEnabled && RateLimiter is not null && ClientOptions.ConnectDelayAfterRateLimited is not null)
|
||||||
|
{
|
||||||
|
var retryAfter = DateTime.UtcNow.Add(ClientOptions.ConnectDelayAfterRateLimited.Value);
|
||||||
|
_logger.AddingRetryAfterGuard(retryAfter);
|
||||||
|
await RateLimiter.SetRetryAfterGuardAsync(retryAfter, RateLimiting.RateLimitItemType.Connection).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Connect a socket
|
/// Connect a socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -27,6 +27,10 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
event Func<int, Task>? OnRequestRateLimited;
|
event Func<int, Task>? OnRequestRateLimited;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// Connection was ratelimited and couldn't be established
|
||||||
|
/// </summary>
|
||||||
|
event Func<Task>? OnConnectRateLimited;
|
||||||
|
/// <summary>
|
||||||
/// Websocket error event
|
/// Websocket error event
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event Func<Exception, Task> OnError;
|
event Func<Exception, Task> OnError;
|
||||||
|
@ -22,6 +22,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
private static readonly Action<ILogger, Exception?> _disposingSocketClient;
|
private static readonly Action<ILogger, Exception?> _disposingSocketClient;
|
||||||
private static readonly Action<ILogger, int, int, Exception?> _unsubscribingSubscription;
|
private static readonly Action<ILogger, int, int, Exception?> _unsubscribingSubscription;
|
||||||
private static readonly Action<ILogger, int, Exception?> _reconnectingAllConnections;
|
private static readonly Action<ILogger, int, Exception?> _reconnectingAllConnections;
|
||||||
|
private static readonly Action<ILogger, DateTime, Exception?> _addingRetryAfterGuard;
|
||||||
|
|
||||||
static SocketApiClientLoggingExtension()
|
static SocketApiClientLoggingExtension()
|
||||||
{
|
{
|
||||||
@ -104,6 +105,11 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
LogLevel.Information,
|
LogLevel.Information,
|
||||||
new EventId(3017, "ReconnectingAll"),
|
new EventId(3017, "ReconnectingAll"),
|
||||||
"Reconnecting all {ConnectionCount} connections");
|
"Reconnecting all {ConnectionCount} connections");
|
||||||
|
|
||||||
|
_addingRetryAfterGuard = LoggerMessage.Define<DateTime>(
|
||||||
|
LogLevel.Warning,
|
||||||
|
new EventId(3018, "AddRetryAfterGuard"),
|
||||||
|
"Adding RetryAfterGuard ({RetryAfter}) because the connection attempt was rate limited");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void FailedToAddSubscriptionRetryOnDifferentConnection(this ILogger logger, int socketId)
|
public static void FailedToAddSubscriptionRetryOnDifferentConnection(this ILogger logger, int socketId)
|
||||||
@ -185,5 +191,10 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
{
|
{
|
||||||
_reconnectingAllConnections(logger, connectionCount, null);
|
_reconnectingAllConnections(logger, connectionCount, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void AddingRetryAfterGuard(this ILogger logger, DateTime retryAfter)
|
||||||
|
{
|
||||||
|
_addingRetryAfterGuard(logger, retryAfter, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,12 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan DelayAfterConnect { get; set; } = TimeSpan.Zero;
|
public TimeSpan DelayAfterConnect { get; set; } = TimeSpan.Zero;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This delay is used to set a RetryAfter guard on the connection after a rate limit is hit on the server.
|
||||||
|
/// This is used to prevent the client from reconnecting too quickly after a rate limit is hit.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan? ConnectDelayAfterRateLimited { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a copy of this options
|
/// Create a copy of this options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -21,25 +21,35 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
|||||||
public string Name => "RetryAfterGuard";
|
public string Name => "RetryAfterGuard";
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Description => $"Pause requests until after {After}";
|
public string Description => $"Pause {Type} until after {After}";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The timestamp after which requests are allowed again
|
/// The timestamp after which requests are allowed again
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime After { get; private set; }
|
public DateTime After { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The type of rate limit item this guard is for
|
||||||
|
/// </summary>
|
||||||
|
public RateLimitItemType Type { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="after"></param>
|
/// <param name="after"></param>
|
||||||
public RetryAfterGuard(DateTime after)
|
/// <param name="type"></param>
|
||||||
|
public RetryAfterGuard(DateTime after, RateLimitItemType type)
|
||||||
{
|
{
|
||||||
After = after;
|
After = after;
|
||||||
|
Type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight)
|
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight)
|
||||||
{
|
{
|
||||||
|
if (type != Type)
|
||||||
|
return LimitCheck.NotApplicable;
|
||||||
|
|
||||||
var dif = (After + _windowBuffer) - DateTime.UtcNow;
|
var dif = (After + _windowBuffer) - DateTime.UtcNow;
|
||||||
if (dif <= TimeSpan.Zero)
|
if (dif <= TimeSpan.Zero)
|
||||||
return LimitCheck.NotApplicable;
|
return LimitCheck.NotApplicable;
|
||||||
|
@ -29,8 +29,9 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
|
|||||||
/// Set a RetryAfter guard, can be used when a server rate limit is hit and a RetryAfter header is specified
|
/// Set a RetryAfter guard, can be used when a server rate limit is hit and a RetryAfter header is specified
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="retryAfter">The time after which requests can be send again</param>
|
/// <param name="retryAfter">The time after which requests can be send again</param>
|
||||||
|
/// <param name="type">RateLimitType</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task SetRetryAfterGuardAsync(DateTime retryAfter);
|
Task SetRetryAfterGuardAsync(DateTime retryAfter, RateLimitItemType type = RateLimitItemType.Request);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the 'retry after' timestamp if set
|
/// Returns the 'retry after' timestamp if set
|
||||||
|
@ -152,7 +152,7 @@ namespace CryptoExchange.Net.RateLimiting
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task SetRetryAfterGuardAsync(DateTime retryAfter)
|
public async Task SetRetryAfterGuardAsync(DateTime retryAfter, RateLimitItemType type)
|
||||||
{
|
{
|
||||||
await _semaphore.WaitAsync().ConfigureAwait(false);
|
await _semaphore.WaitAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
@ -160,7 +160,7 @@ namespace CryptoExchange.Net.RateLimiting
|
|||||||
{
|
{
|
||||||
var retryAfterGuard = _guards.OfType<RetryAfterGuard>().SingleOrDefault();
|
var retryAfterGuard = _guards.OfType<RetryAfterGuard>().SingleOrDefault();
|
||||||
if (retryAfterGuard == null)
|
if (retryAfterGuard == null)
|
||||||
_guards.Add(new RetryAfterGuard(retryAfter));
|
_guards.Add(new RetryAfterGuard(retryAfter, type));
|
||||||
else
|
else
|
||||||
retryAfterGuard.UpdateAfter(retryAfter);
|
retryAfterGuard.UpdateAfter(retryAfter);
|
||||||
}
|
}
|
||||||
|
@ -116,6 +116,9 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public event Func<int, Task>? OnRequestRateLimited;
|
public event Func<int, Task>? OnRequestRateLimited;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public event Func<Task>? OnConnectRateLimited;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public event Func<Exception, Task>? OnError;
|
public event Func<Exception, Task>? OnError;
|
||||||
|
|
||||||
@ -186,6 +189,9 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
socket.Options.SetBuffer(_receiveBufferSize, _sendBufferSize);
|
socket.Options.SetBuffer(_receiveBufferSize, _sendBufferSize);
|
||||||
if (Parameters.Proxy != null)
|
if (Parameters.Proxy != null)
|
||||||
SetProxy(socket, Parameters.Proxy);
|
SetProxy(socket, Parameters.Proxy);
|
||||||
|
#if NET6_0_OR_GREATER
|
||||||
|
socket.Options.CollectHttpResponseDetails = true;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
catch (PlatformNotSupportedException)
|
catch (PlatformNotSupportedException)
|
||||||
{
|
{
|
||||||
@ -220,6 +226,26 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
// if _ctsSource was canceled this was already logged
|
// if _ctsSource was canceled this was already logged
|
||||||
_logger.SocketConnectionFailed(Id, e.Message, e);
|
_logger.SocketConnectionFailed(Id, e.Message, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e is WebSocketException we)
|
||||||
|
{
|
||||||
|
#if (NET6_0_OR_GREATER)
|
||||||
|
if (_socket.HttpStatusCode == HttpStatusCode.TooManyRequests)
|
||||||
|
{
|
||||||
|
await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
||||||
|
return new CallResult(new ServerRateLimitError(we.Message));
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
// ClientWebSocket.HttpStatusCode is only available in .NET6+ https://learn.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocket.httpstatuscode?view=net-8.0
|
||||||
|
// Try to read 429 from the message instead
|
||||||
|
if (we.Message.Contains("429"))
|
||||||
|
{
|
||||||
|
await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
||||||
|
return new CallResult(new ServerRateLimitError(we.Message));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
return new CallResult(new CantConnectError());
|
return new CallResult(new CantConnectError());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,6 +71,11 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action<IMessageAccessor>? UnhandledMessage;
|
public event Action<IMessageAccessor>? UnhandledMessage;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Connection was rate limited and couldn't be established
|
||||||
|
/// </summary>
|
||||||
|
public Func<Task>? ConnectRateLimitedAsync;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The amount of subscriptions on this connection
|
/// The amount of subscriptions on this connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -222,6 +227,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
_socket.OnStreamMessage += HandleStreamMessage;
|
_socket.OnStreamMessage += HandleStreamMessage;
|
||||||
_socket.OnRequestSent += HandleRequestSentAsync;
|
_socket.OnRequestSent += HandleRequestSentAsync;
|
||||||
_socket.OnRequestRateLimited += HandleRequestRateLimitedAsync;
|
_socket.OnRequestRateLimited += HandleRequestRateLimitedAsync;
|
||||||
|
_socket.OnConnectRateLimited += HandleConnectRateLimitedAsync;
|
||||||
_socket.OnOpen += HandleOpenAsync;
|
_socket.OnOpen += HandleOpenAsync;
|
||||||
_socket.OnClose += HandleCloseAsync;
|
_socket.OnClose += HandleCloseAsync;
|
||||||
_socket.OnReconnecting += HandleReconnectingAsync;
|
_socket.OnReconnecting += HandleReconnectingAsync;
|
||||||
@ -385,6 +391,16 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler for whenever a connection was rate limited and couldn't be established
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected async virtual Task HandleConnectRateLimitedAsync()
|
||||||
|
{
|
||||||
|
if (ConnectRateLimitedAsync is not null)
|
||||||
|
await ConnectRateLimitedAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handler for whenever a request is sent over the websocket
|
/// Handler for whenever a request is sent over the websocket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -20,6 +20,7 @@ namespace CryptoExchange.Net.Testing.Implementations
|
|||||||
public event Func<Task>? OnReconnected;
|
public event Func<Task>? OnReconnected;
|
||||||
public event Func<Task>? OnReconnecting;
|
public event Func<Task>? OnReconnecting;
|
||||||
public event Func<int, Task>? OnRequestRateLimited;
|
public event Func<int, Task>? OnRequestRateLimited;
|
||||||
|
public event Func<Task>? OnConnectRateLimited;
|
||||||
public event Func<Exception, Task>? OnError;
|
public event Func<Exception, Task>? OnError;
|
||||||
#pragma warning restore 0067
|
#pragma warning restore 0067
|
||||||
public event Func<int, Task>? OnRequestSent;
|
public event Func<int, Task>? OnRequestSent;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user