1
0
mirror of https://github.com/JKorf/CryptoExchange.Net synced 2025-06-07 16:06:15 +00:00

Added RetryAfter property for ratelimit errors, added parsing of rate limit return

This commit is contained in:
JKorf 2023-08-21 21:34:26 +02:00
parent 262c4e4aa5
commit 468cd5e48e
3 changed files with 87 additions and 14 deletions

View File

@ -4,6 +4,7 @@ using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -344,8 +345,13 @@ namespace CryptoExchange.Net
_logger.Log(LogLevel.Warning, $"[{request.RequestId}] Error received in {sw.ElapsedMilliseconds}ms: {data}"); _logger.Log(LogLevel.Warning, $"[{request.RequestId}] Error received in {sw.ElapsedMilliseconds}ms: {data}");
responseStream.Close(); responseStream.Close();
response.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) if (error.Code == null || error.Code == 0)
error.Code = (int)response.StatusCode; error.Code = (int)response.StatusCode;
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, data.Length, data, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error); return new WebCallResult<T>(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
} }
/// <summary> /// <summary>
/// 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)
/// </summary> /// </summary>
/// <param name="error">The string the request returned</param> /// <param name="httpStatusCode">The response status code</param>
/// <param name="responseHeaders">The response headers</param>
/// <param name="data">The response data</param>
/// <returns></returns> /// <returns></returns>
protected virtual Error ParseErrorResponse(JToken error) protected virtual Error ParseErrorResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, string data)
{ {
return new ServerError(error.ToString()); return new ServerError(data);
}
/// <summary>
/// Parse a rate limit error response from the server. Only used when server returns http status 429 or 418
/// </summary>
/// <param name="httpStatusCode">The response status code</param>
/// <param name="responseHeaders">The response headers</param>
/// <param name="data">The response data</param>
/// <returns></returns>
protected virtual Error ParseRateLimitResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> 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);
} }
/// <summary> /// <summary>

View File

@ -1,4 +1,6 @@
namespace CryptoExchange.Net.Objects using System;
namespace CryptoExchange.Net.Objects
{ {
/// <summary> /// <summary>
/// Base class for errors /// Base class for errors
@ -202,15 +204,14 @@
} }
/// <summary> /// <summary>
/// Rate limit exceeded /// Rate limit exceeded (client side)
/// </summary> /// </summary>
public class RateLimitError : Error public abstract class BaseRateLimitError : Error
{ {
/// <summary> /// <summary>
/// ctor /// When the request can be retried
/// </summary> /// </summary>
/// <param name="message"></param> public DateTime? RetryAfter { get; set; }
public RateLimitError(string message) : base(null, "Rate limit exceeded: " + message, null) { }
/// <summary> /// <summary>
/// ctor /// ctor
@ -218,7 +219,47 @@
/// <param name="code"></param> /// <param name="code"></param>
/// <param name="message"></param> /// <param name="message"></param>
/// <param name="data"></param> /// <param name="data"></param>
protected RateLimitError(int? code, string message, object? data): base(code, message, data) { } protected BaseRateLimitError(int? code, string message, object? data) : base(code, message, data) { }
}
/// <summary>
/// Rate limit exceeded (client side)
/// </summary>
public class ClientRateLimitError : BaseRateLimitError
{
/// <summary>
/// ctor
/// </summary>
/// <param name="message"></param>
public ClientRateLimitError(string message) : base(null, "Client rate limit exceeded: " + message, null) { }
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected ClientRateLimitError(int? code, string message, object? data): base(code, message, data) { }
}
/// <summary>
/// Rate limit exceeded (server side)
/// </summary>
public class ServerRateLimitError : BaseRateLimitError
{
/// <summary>
/// ctor
/// </summary>
/// <param name="message"></param>
public ServerRateLimitError(string message) : base(null, "Server rate limit exceeded: " + message, null) { }
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected ServerRateLimitError(int? code, string message, object? data) : base(code, message, data) { }
} }
/// <summary> /// <summary>

View File

@ -256,7 +256,7 @@ namespace CryptoExchange.Net.Objects
historyTopic.Semaphore.Release(); historyTopic.Semaphore.Release();
var msg = $"Request to {endpoint} failed because of rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}"; 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); logger.Log(LogLevel.Warning, msg);
return new CallResult<int>(new RateLimitError(msg)); return new CallResult<int>(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}"); logger.Log(LogLevel.Information, $"Request to {endpoint} waiting {thisWaitTime}ms for rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}");