1
0
mirror of https://github.com/JKorf/CryptoExchange.Net synced 2026-04-07 02:01:12 +00:00

Added parsing of REST response data up to 128 characters for error responses

This commit is contained in:
JKorf 2026-02-22 16:05:49 +01:00
parent 6d3e72745a
commit 36c2411d46
5 changed files with 66 additions and 22 deletions

View File

@ -81,6 +81,25 @@ namespace CryptoExchange.Net.UnitTests
Assert.That(result.Error is ServerError);
}
[TestCase]
public async Task ReceivingErrorAndNotParsingErrorAndInvalidJson_Should_ContainData()
{
// arrange
var client = new TestRestClient();
var response = "<html>...</html>";
client.SetErrorWithResponse(response, System.Net.HttpStatusCode.BadRequest);
// act
var result = await client.Api1.Request<TestObject>();
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
Assert.That(result.Error is ServerError);
Assert.That(result.Error.Message.Contains(response));
}
[TestCase]
public async Task ReceivingErrorAndParsingError_Should_ResultInParsedError()
{

View File

@ -19,11 +19,14 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
private ErrorMapping _errorMapping = new ErrorMapping([]);
public override JsonSerializerOptions Options => new JsonSerializerOptions();
public override ValueTask<Error> ParseErrorResponse(int httpStatusCode, HttpResponseHeaders responseHeaders, Stream responseStream)
public override async ValueTask<Error> ParseErrorResponse(int httpStatusCode, HttpResponseHeaders responseHeaders, Stream responseStream)
{
var errorData = JsonSerializer.Deserialize<TestError>(responseStream);
var result = await GetJsonDocument(responseStream).ConfigureAwait(false);
if (result.Item1 != null)
return result.Item1;
return new ValueTask<Error>(new ServerError(errorData.ErrorCode, _errorMapping.GetErrorInfo(errorData.ErrorCode.ToString(), errorData.ErrorMessage)));
var errorData = result.Item2.Deserialize<TestError>();
return new ServerError(errorData.ErrorCode, _errorMapping.GetErrorInfo(errorData.ErrorCode.ToString(), errorData.ErrorMessage));
}
}
}

View File

@ -437,23 +437,15 @@ namespace CryptoExchange.Net.Clients
responseStream = await response.GetResponseStreamAsync(cancellationToken).ConfigureAwait(false);
string? originalData = null;
var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData;
if (outputOriginalData || MessageHandler.RequiresSeekableStream)
if (outputOriginalData || MessageHandler.RequiresSeekableStream || !response.IsSuccessStatusCode)
{
// If we want to return the original string data from the stream, but still want to process it
// we'll need to copy it as the stream isn't seekable, and thus we can only read it once
var memoryStream = new MemoryStream();
await responseStream.CopyToAsync(memoryStream).ConfigureAwait(false);
using var reader = new StreamReader(memoryStream, Encoding.UTF8, false, 4096, true);
responseStream = await CopyStreamAsync(responseStream).ConfigureAwait(false);
using var reader = new StreamReader(responseStream, Encoding.UTF8, false, 4096, true);
if (outputOriginalData)
{
memoryStream.Position = 0;
originalData = await reader.ReadToEndAsync().ConfigureAwait(false);
responseStream.Position = 0;
}
// Continue processing from the memory stream since the response stream is already read and we can't seek it
responseStream.Close();
memoryStream.Position = 0;
responseStream = memoryStream;
}
if (!response.IsSuccessStatusCode && !requestDefinition.TryParseOnNonSuccess)
@ -479,7 +471,6 @@ namespace CryptoExchange.Net.Clients
else
{
// Handle a 'normal' error response. Can still be either a json error message or some random HTML or other string
try
{
error = await MessageHandler.ParseErrorResponse(
@ -769,6 +760,15 @@ namespace CryptoExchange.Net.Clients
}
}
private async Task<Stream> CopyStreamAsync(Stream responseStream)
{
var memoryStream = new MemoryStream();
await responseStream.CopyToAsync(memoryStream).ConfigureAwait(false);
responseStream.Close();
memoryStream.Position = 0;
return memoryStream;
}
private bool ShouldCache(RequestDefinition definition)
=> ClientOptions.CachingEnabled
&& definition.Method == HttpMethod.Get

View File

@ -18,6 +18,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
public abstract class JsonRestMessageHandler : IRestMessageHandler
{
private static MediaTypeWithQualityHeaderValue _acceptJsonContent = new MediaTypeWithQualityHeaderValue(Constants.JsonContentHeader);
private const int _errorResponseSnippetLimit = 128;
/// <summary>
/// Empty rate limit error
@ -80,7 +81,20 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
}
catch (Exception ex)
{
return (new ServerError(new ErrorInfo(ErrorType.DeserializationFailed, false, "Deserialization failed, invalid JSON"), ex), null);
var errorMsg = "Deserialization failed, invalid JSON";
if (stream.CanSeek)
{
var dataSnippet = new char[_errorResponseSnippetLimit];
stream.Seek(0, SeekOrigin.Begin);
var written = new StreamReader(stream).ReadBlock(dataSnippet, 0, _errorResponseSnippetLimit);
var data = new string(dataSnippet, 0, written);
errorMsg += $": {data}";
if (data.Length == _errorResponseSnippetLimit)
errorMsg += " (truncated)";
}
var error = new DeserializeError(errorMsg, ex);
return (error, null);
}
}

View File

@ -211,7 +211,15 @@ namespace CryptoExchange.Net.Objects
/// <summary>
/// ctor
/// </summary>
public DeserializeError(string? message = null, Exception? exception = null) : base(null, _errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { }
public DeserializeError(string? message = null, Exception? exception = null)
: base(null,
_errorInfo with
{
Message = message?.Length > 0
? message
: _errorInfo.Message
},
exception) { }
}
/// <summary>