From 468cd5e48eb46bbe65d56ab22a7599ee702d854e Mon Sep 17 00:00:00 2001 From: JKorf Date: Mon, 21 Aug 2023 21:34:26 +0200 Subject: [PATCH] Added RetryAfter property for ratelimit errors, added parsing of rate limit return --- CryptoExchange.Net/Clients/RestApiClient.cs | 44 ++++++++++++++--- CryptoExchange.Net/Objects/Error.cs | 55 ++++++++++++++++++--- CryptoExchange.Net/Objects/RateLimiter.cs | 2 +- 3 files changed, 87 insertions(+), 14 deletions(-) diff --git a/CryptoExchange.Net/Clients/RestApiClient.cs b/CryptoExchange.Net/Clients/RestApiClient.cs index 3a4d9ea..67b0247 100644 --- a/CryptoExchange.Net/Clients/RestApiClient.cs +++ b/CryptoExchange.Net/Clients/RestApiClient.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -344,8 +345,13 @@ namespace CryptoExchange.Net _logger.Log(LogLevel.Warning, $"[{request.RequestId}] Error received in {sw.ElapsedMilliseconds}ms: {data}"); responseStream.Close(); response.Close(); - var parseResult = ValidateJson(data); - var error = parseResult.Success ? ParseErrorResponse(parseResult.Data) : new ServerError(data)!; + + Error error; + if (response.StatusCode == (HttpStatusCode)418 || response.StatusCode == (HttpStatusCode)429) + error = ParseRateLimitResponse((int)response.StatusCode, response.ResponseHeaders, data); + else + error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, data); + if (error.Code == null || error.Code == 0) error.Code = (int)response.StatusCode; return new WebCallResult(statusCode, headers, sw.Elapsed, data.Length, data, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error); @@ -529,13 +535,39 @@ namespace CryptoExchange.Net } /// - /// Parse an error response from the server. Only used when server returns a status other than Success(200) + /// Parse an error response from the server. Only used when server returns a status other than Success(200) or ratelimit error (429 or 418) /// - /// The string the request returned + /// The response status code + /// The response headers + /// The response data /// - protected virtual Error ParseErrorResponse(JToken error) + protected virtual Error ParseErrorResponse(int httpStatusCode, IEnumerable>> responseHeaders, string data) { - return new ServerError(error.ToString()); + return new ServerError(data); + } + + /// + /// Parse a rate limit error response from the server. Only used when server returns http status 429 or 418 + /// + /// The response status code + /// The response headers + /// The response data + /// + protected virtual Error ParseRateLimitResponse(int httpStatusCode, IEnumerable>> responseHeaders, string data) + { + // Handle retry after header + var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase)); + if (!retryAfterHeader.Value.Any()) + return new ServerRateLimitError(data); + + var value = retryAfterHeader.Value.First(); + if (int.TryParse(value, out var seconds)) + return new ServerRateLimitError(data) { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) }; + + if (DateTime.TryParse(value, out var datetime)) + return new ServerRateLimitError(data) { RetryAfter = datetime }; + + return new ServerRateLimitError(data); } /// diff --git a/CryptoExchange.Net/Objects/Error.cs b/CryptoExchange.Net/Objects/Error.cs index e3791a3..dea1799 100644 --- a/CryptoExchange.Net/Objects/Error.cs +++ b/CryptoExchange.Net/Objects/Error.cs @@ -1,4 +1,6 @@ -namespace CryptoExchange.Net.Objects +using System; + +namespace CryptoExchange.Net.Objects { /// /// Base class for errors @@ -202,15 +204,14 @@ } /// - /// Rate limit exceeded + /// Rate limit exceeded (client side) /// - public class RateLimitError : Error + public abstract class BaseRateLimitError : Error { /// - /// ctor + /// When the request can be retried /// - /// - public RateLimitError(string message) : base(null, "Rate limit exceeded: " + message, null) { } + public DateTime? RetryAfter { get; set; } /// /// ctor @@ -218,7 +219,47 @@ /// /// /// - protected RateLimitError(int? code, string message, object? data): base(code, message, data) { } + protected BaseRateLimitError(int? code, string message, object? data) : base(code, message, data) { } + } + + /// + /// Rate limit exceeded (client side) + /// + public class ClientRateLimitError : BaseRateLimitError + { + /// + /// ctor + /// + /// + public ClientRateLimitError(string message) : base(null, "Client rate limit exceeded: " + message, null) { } + + /// + /// ctor + /// + /// + /// + /// + protected ClientRateLimitError(int? code, string message, object? data): base(code, message, data) { } + } + + /// + /// Rate limit exceeded (server side) + /// + public class ServerRateLimitError : BaseRateLimitError + { + /// + /// ctor + /// + /// + public ServerRateLimitError(string message) : base(null, "Server rate limit exceeded: " + message, null) { } + + /// + /// ctor + /// + /// + /// + /// + protected ServerRateLimitError(int? code, string message, object? data) : base(code, message, data) { } } /// diff --git a/CryptoExchange.Net/Objects/RateLimiter.cs b/CryptoExchange.Net/Objects/RateLimiter.cs index 530aa4e..a54a799 100644 --- a/CryptoExchange.Net/Objects/RateLimiter.cs +++ b/CryptoExchange.Net/Objects/RateLimiter.cs @@ -256,7 +256,7 @@ namespace CryptoExchange.Net.Objects historyTopic.Semaphore.Release(); var msg = $"Request to {endpoint} failed because of rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}"; logger.Log(LogLevel.Warning, msg); - return new CallResult(new RateLimitError(msg)); + return new CallResult(new ClientRateLimitError(msg) { RetryAfter = DateTime.UtcNow.AddSeconds(thisWaitTime) }); } logger.Log(LogLevel.Information, $"Request to {endpoint} waiting {thisWaitTime}ms for rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}");