diff --git a/CryptoExchange.Net/Clients/SocketApiClient.cs b/CryptoExchange.Net/Clients/SocketApiClient.cs
index c2ee56a..e92572c 100644
--- a/CryptoExchange.Net/Clients/SocketApiClient.cs
+++ b/CryptoExchange.Net/Clients/SocketApiClient.cs
@@ -519,6 +519,7 @@ namespace CryptoExchange.Net.Clients
var socket = CreateSocket(connectionAddress.Data!);
var socketConnection = new SocketConnection(_logger, this, socket, address);
socketConnection.UnhandledMessage += HandleUnhandledMessage;
+ socketConnection.ConnectRateLimitedAsync += HandleConnectRateLimitedAsync;
socketConnection.DedicatedRequestConnection = dedicatedRequestConnection;
foreach (var ptg in PeriodicTaskRegistrations)
@@ -538,6 +539,19 @@ namespace CryptoExchange.Net.Clients
{
}
+ ///
+ /// Process connect rate limited
+ ///
+ 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);
+ }
+ }
+
///
/// Connect a socket
///
diff --git a/CryptoExchange.Net/Interfaces/IWebsocket.cs b/CryptoExchange.Net/Interfaces/IWebsocket.cs
index f38f917..b1e75cb 100644
--- a/CryptoExchange.Net/Interfaces/IWebsocket.cs
+++ b/CryptoExchange.Net/Interfaces/IWebsocket.cs
@@ -27,6 +27,10 @@ namespace CryptoExchange.Net.Interfaces
///
event Func? OnRequestRateLimited;
///
+ /// Connection was ratelimited and couldn't be established
+ ///
+ event Func? OnConnectRateLimited;
+ ///
/// Websocket error event
///
event Func OnError;
diff --git a/CryptoExchange.Net/Logging/Extensions/SocketApiClientLoggingExtension.cs b/CryptoExchange.Net/Logging/Extensions/SocketApiClientLoggingExtension.cs
index 3590a17..c8e553a 100644
--- a/CryptoExchange.Net/Logging/Extensions/SocketApiClientLoggingExtension.cs
+++ b/CryptoExchange.Net/Logging/Extensions/SocketApiClientLoggingExtension.cs
@@ -22,6 +22,7 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action _disposingSocketClient;
private static readonly Action _unsubscribingSubscription;
private static readonly Action _reconnectingAllConnections;
+ private static readonly Action _addingRetryAfterGuard;
static SocketApiClientLoggingExtension()
{
@@ -104,6 +105,11 @@ namespace CryptoExchange.Net.Logging.Extensions
LogLevel.Information,
new EventId(3017, "ReconnectingAll"),
"Reconnecting all {ConnectionCount} connections");
+
+ _addingRetryAfterGuard = LoggerMessage.Define(
+ 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)
@@ -185,5 +191,10 @@ namespace CryptoExchange.Net.Logging.Extensions
{
_reconnectingAllConnections(logger, connectionCount, null);
}
+
+ public static void AddingRetryAfterGuard(this ILogger logger, DateTime retryAfter)
+ {
+ _addingRetryAfterGuard(logger, retryAfter, null);
+ }
}
}
diff --git a/CryptoExchange.Net/Objects/Options/SocketExchangeOptions.cs b/CryptoExchange.Net/Objects/Options/SocketExchangeOptions.cs
index f67200f..5588bea 100644
--- a/CryptoExchange.Net/Objects/Options/SocketExchangeOptions.cs
+++ b/CryptoExchange.Net/Objects/Options/SocketExchangeOptions.cs
@@ -47,6 +47,12 @@ namespace CryptoExchange.Net.Objects.Options
///
public TimeSpan DelayAfterConnect { get; set; } = TimeSpan.Zero;
+ ///
+ /// 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.
+ ///
+ public TimeSpan? ConnectDelayAfterRateLimited { get; set; }
+
///
/// Create a copy of this options
///
diff --git a/CryptoExchange.Net/RateLimiting/Guards/RetryAfterGuard.cs b/CryptoExchange.Net/RateLimiting/Guards/RetryAfterGuard.cs
index b09763f..23a9fdc 100644
--- a/CryptoExchange.Net/RateLimiting/Guards/RetryAfterGuard.cs
+++ b/CryptoExchange.Net/RateLimiting/Guards/RetryAfterGuard.cs
@@ -21,25 +21,35 @@ namespace CryptoExchange.Net.RateLimiting.Guards
public string Name => "RetryAfterGuard";
///
- public string Description => $"Pause requests until after {After}";
+ public string Description => $"Pause {Type} until after {After}";
///
/// The timestamp after which requests are allowed again
///
public DateTime After { get; private set; }
+ ///
+ /// The type of rate limit item this guard is for
+ ///
+ public RateLimitItemType Type { get; private set; }
+
///
/// ctor
///
///
- public RetryAfterGuard(DateTime after)
+ ///
+ public RetryAfterGuard(DateTime after, RateLimitItemType type)
{
After = after;
+ Type = type;
}
///
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;
if (dif <= TimeSpan.Zero)
return LimitCheck.NotApplicable;
diff --git a/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGate.cs b/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGate.cs
index eb1d754..536ebd6 100644
--- a/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGate.cs
+++ b/CryptoExchange.Net/RateLimiting/Interfaces/IRateLimitGate.cs
@@ -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
///
/// The time after which requests can be send again
+ /// RateLimitType
///
- Task SetRetryAfterGuardAsync(DateTime retryAfter);
+ Task SetRetryAfterGuardAsync(DateTime retryAfter, RateLimitItemType type = RateLimitItemType.Request);
///
/// Returns the 'retry after' timestamp if set
diff --git a/CryptoExchange.Net/RateLimiting/RateLimitGate.cs b/CryptoExchange.Net/RateLimiting/RateLimitGate.cs
index ebde779..c73ea67 100644
--- a/CryptoExchange.Net/RateLimiting/RateLimitGate.cs
+++ b/CryptoExchange.Net/RateLimiting/RateLimitGate.cs
@@ -152,7 +152,7 @@ namespace CryptoExchange.Net.RateLimiting
}
///
- public async Task SetRetryAfterGuardAsync(DateTime retryAfter)
+ public async Task SetRetryAfterGuardAsync(DateTime retryAfter, RateLimitItemType type)
{
await _semaphore.WaitAsync().ConfigureAwait(false);
@@ -160,7 +160,7 @@ namespace CryptoExchange.Net.RateLimiting
{
var retryAfterGuard = _guards.OfType().SingleOrDefault();
if (retryAfterGuard == null)
- _guards.Add(new RetryAfterGuard(retryAfter));
+ _guards.Add(new RetryAfterGuard(retryAfter, type));
else
retryAfterGuard.UpdateAfter(retryAfter);
}
diff --git a/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs b/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs
index cd96adb..3c0a445 100644
--- a/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs
+++ b/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs
@@ -116,6 +116,9 @@ namespace CryptoExchange.Net.Sockets
///
public event Func? OnRequestRateLimited;
+ ///
+ public event Func? OnConnectRateLimited;
+
///
public event Func? OnError;
@@ -186,6 +189,9 @@ namespace CryptoExchange.Net.Sockets
socket.Options.SetBuffer(_receiveBufferSize, _sendBufferSize);
if (Parameters.Proxy != null)
SetProxy(socket, Parameters.Proxy);
+ #if NET6_0_OR_GREATER
+ socket.Options.CollectHttpResponseDetails = true;
+ #endif
}
catch (PlatformNotSupportedException)
{
@@ -220,6 +226,26 @@ namespace CryptoExchange.Net.Sockets
// if _ctsSource was canceled this was already logged
_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());
}
diff --git a/CryptoExchange.Net/Sockets/SocketConnection.cs b/CryptoExchange.Net/Sockets/SocketConnection.cs
index 20e2928..4018574 100644
--- a/CryptoExchange.Net/Sockets/SocketConnection.cs
+++ b/CryptoExchange.Net/Sockets/SocketConnection.cs
@@ -71,6 +71,11 @@ namespace CryptoExchange.Net.Sockets
///
public event Action? UnhandledMessage;
+ ///
+ /// Connection was rate limited and couldn't be established
+ ///
+ public Func? ConnectRateLimitedAsync;
+
///
/// The amount of subscriptions on this connection
///
@@ -222,6 +227,7 @@ namespace CryptoExchange.Net.Sockets
_socket.OnStreamMessage += HandleStreamMessage;
_socket.OnRequestSent += HandleRequestSentAsync;
_socket.OnRequestRateLimited += HandleRequestRateLimitedAsync;
+ _socket.OnConnectRateLimited += HandleConnectRateLimitedAsync;
_socket.OnOpen += HandleOpenAsync;
_socket.OnClose += HandleCloseAsync;
_socket.OnReconnecting += HandleReconnectingAsync;
@@ -385,6 +391,16 @@ namespace CryptoExchange.Net.Sockets
return Task.CompletedTask;
}
+ ///
+ /// Handler for whenever a connection was rate limited and couldn't be established
+ ///
+ ///
+ protected async virtual Task HandleConnectRateLimitedAsync()
+ {
+ if (ConnectRateLimitedAsync is not null)
+ await ConnectRateLimitedAsync().ConfigureAwait(false);
+ }
+
///
/// Handler for whenever a request is sent over the websocket
///
diff --git a/CryptoExchange.Net/Testing/Implementations/TestSocket.cs b/CryptoExchange.Net/Testing/Implementations/TestSocket.cs
index f2806e6..b65cfa3 100644
--- a/CryptoExchange.Net/Testing/Implementations/TestSocket.cs
+++ b/CryptoExchange.Net/Testing/Implementations/TestSocket.cs
@@ -20,6 +20,7 @@ namespace CryptoExchange.Net.Testing.Implementations
public event Func? OnReconnected;
public event Func? OnReconnecting;
public event Func? OnRequestRateLimited;
+ public event Func? OnConnectRateLimited;
public event Func? OnError;
#pragma warning restore 0067
public event Func? OnRequestSent;