diff --git a/CryptoExchange.Net.UnitTests/RestClientTests.cs b/CryptoExchange.Net.UnitTests/RestClientTests.cs index e7c790b..555cf70 100644 --- a/CryptoExchange.Net.UnitTests/RestClientTests.cs +++ b/CryptoExchange.Net.UnitTests/RestClientTests.cs @@ -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 = "..."; + client.SetErrorWithResponse(response, System.Net.HttpStatusCode.BadRequest); + + // act + var result = await client.Api1.Request(); + + // 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() { diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestMessageHandler.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestMessageHandler.cs index d2ea822..bf4be5b 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestMessageHandler.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestMessageHandler.cs @@ -19,11 +19,14 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations private ErrorMapping _errorMapping = new ErrorMapping([]); public override JsonSerializerOptions Options => new JsonSerializerOptions(); - public override ValueTask ParseErrorResponse(int httpStatusCode, HttpResponseHeaders responseHeaders, Stream responseStream) + public override async ValueTask ParseErrorResponse(int httpStatusCode, HttpResponseHeaders responseHeaders, Stream responseStream) { - var errorData = JsonSerializer.Deserialize(responseStream); + var result = await GetJsonDocument(responseStream).ConfigureAwait(false); + if (result.Item1 != null) + return result.Item1; - return new ValueTask(new ServerError(errorData.ErrorCode, _errorMapping.GetErrorInfo(errorData.ErrorCode.ToString(), errorData.ErrorMessage))); + var errorData = result.Item2.Deserialize(); + return new ServerError(errorData.ErrorCode, _errorMapping.GetErrorInfo(errorData.ErrorCode.ToString(), errorData.ErrorMessage)); } } } diff --git a/CryptoExchange.Net/Clients/RestApiClient.cs b/CryptoExchange.Net/Clients/RestApiClient.cs index 3598c83..1efa638 100644 --- a/CryptoExchange.Net/Clients/RestApiClient.cs +++ b/CryptoExchange.Net/Clients/RestApiClient.cs @@ -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); - if (outputOriginalData) + 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,13 +471,12 @@ 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( - (int)response.StatusCode, - response.ResponseHeaders, - responseStream).ConfigureAwait(false); + (int)response.StatusCode, + response.ResponseHeaders, + responseStream).ConfigureAwait(false); } catch (Exception ex) { @@ -769,6 +760,15 @@ namespace CryptoExchange.Net.Clients } } + private async Task 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 diff --git a/CryptoExchange.Net/Converters/SystemTextJson/MessageHandlers/JsonRestMessageHandler.cs b/CryptoExchange.Net/Converters/SystemTextJson/MessageHandlers/JsonRestMessageHandler.cs index 8f36280..e9751b9 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/MessageHandlers/JsonRestMessageHandler.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/MessageHandlers/JsonRestMessageHandler.cs @@ -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; /// /// 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); } } diff --git a/CryptoExchange.Net/Objects/Error.cs b/CryptoExchange.Net/Objects/Error.cs index ea72909..520f2de 100644 --- a/CryptoExchange.Net/Objects/Error.cs +++ b/CryptoExchange.Net/Objects/Error.cs @@ -211,7 +211,15 @@ namespace CryptoExchange.Net.Objects /// /// ctor /// - 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) { } } ///