From d44a11c44e77d0786f55b5329c8ab8a113561323 Mon Sep 17 00:00:00 2001 From: Jkorf Date: Mon, 1 Sep 2025 10:12:59 +0200 Subject: [PATCH] HttpVersion update Added LibraryHelpers.CreateHttpClientMessageHandle to standardize HttpMessageHandler creation Added REST client option for selecting HTTP protocol version Added REST client option for HTTP client keep alive interval Added HttpVersion to WebCallResult responses Updated request logic to default to using HTTP version 2.0 for dotnet core --- .../TestImplementations/TestRestClient.cs | 12 ++-- CryptoExchange.Net/Clients/RestApiClient.cs | 53 +++++++++++------ CryptoExchange.Net/Interfaces/IRequest.cs | 4 ++ .../Interfaces/IRequestFactory.cs | 15 ++--- CryptoExchange.Net/Interfaces/IResponse.cs | 8 ++- CryptoExchange.Net/LibraryHelpers.cs | 58 ++++++++++++++++++- CryptoExchange.Net/Objects/CallResult.cs | 47 +++++++-------- .../Objects/Options/RestExchangeOptions.cs | 18 ++++++ CryptoExchange.Net/Requests/Request.cs | 7 ++- CryptoExchange.Net/Requests/RequestFactory.cs | 42 +++++--------- CryptoExchange.Net/Requests/Response.cs | 6 +- .../SharedApis/Models/ExchangeWebResult.cs | 6 +- .../Testing/Implementations/TestRequest.cs | 2 + .../Implementations/TestRequestFactory.cs | 9 +-- .../Testing/Implementations/TestResponse.cs | 3 + 15 files changed, 200 insertions(+), 90 deletions(-) diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs index ed1227c..d52d8c3 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs @@ -60,7 +60,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations request.Setup(c => c.GetHeaders()).Returns(() => headers.ToArray()); var factory = Mock.Get(Api1.RequestFactory); - factory.Setup(c => c.Create(It.IsAny(), It.IsAny(), It.IsAny())) + factory.Setup(c => c.Create(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Callback((method, uri, id) => { request.Setup(a => a.Uri).Returns(uri); @@ -69,7 +69,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations .Returns(request.Object); factory = Mock.Get(Api2.RequestFactory); - factory.Setup(c => c.Create(It.IsAny(), It.IsAny(), It.IsAny())) + factory.Setup(c => c.Create(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Callback((method, uri, id) => { request.Setup(a => a.Uri).Returns(uri); @@ -90,12 +90,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations request.Setup(c => c.GetResponseAsync(It.IsAny())).Throws(we); var factory = Mock.Get(Api1.RequestFactory); - factory.Setup(c => c.Create(It.IsAny(), It.IsAny(), It.IsAny())) + factory.Setup(c => c.Create(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(request.Object); factory = Mock.Get(Api2.RequestFactory); - factory.Setup(c => c.Create(It.IsAny(), It.IsAny(), It.IsAny())) + factory.Setup(c => c.Create(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(request.Object); } @@ -118,12 +118,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations request.Setup(c => c.GetHeaders()).Returns(headers.ToArray()); var factory = Mock.Get(Api1.RequestFactory); - factory.Setup(c => c.Create(It.IsAny(), It.IsAny(), It.IsAny())) + factory.Setup(c => c.Create(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Callback((method, uri, id) => request.Setup(a => a.Uri).Returns(uri)) .Returns(request.Object); factory = Mock.Get(Api2.RequestFactory); - factory.Setup(c => c.Create(It.IsAny(), It.IsAny(), It.IsAny())) + factory.Setup(c => c.Create(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Callback((method, uri, id) => request.Setup(a => a.Uri).Returns(uri)) .Returns(request.Object); } diff --git a/CryptoExchange.Net/Clients/RestApiClient.cs b/CryptoExchange.Net/Clients/RestApiClient.cs index aa98487..d035907 100644 --- a/CryptoExchange.Net/Clients/RestApiClient.cs +++ b/CryptoExchange.Net/Clients/RestApiClient.cs @@ -106,7 +106,7 @@ namespace CryptoExchange.Net.Clients options, apiOptions) { - RequestFactory.Configure(options.Proxy, options.RequestTimeout, httpClient); + RequestFactory.Configure(options, httpClient); } /// @@ -388,7 +388,7 @@ namespace CryptoExchange.Net.Clients queryString = $"?{queryString}"; var uri = new Uri(baseAddress.AppendPath(definition.Path) + queryString); - var request = RequestFactory.Create(definition.Method, uri, requestId); + var request = RequestFactory.Create(ClientOptions.HttpVersion, definition.Method, uri, requestId); request.Accept = Constants.JsonContentHeader; foreach (var header in requestConfiguration.Headers) @@ -443,9 +443,6 @@ namespace CryptoExchange.Net.Clients { response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false); sw.Stop(); - var statusCode = response.StatusCode; - var headers = response.ResponseHeaders; - var responseLength = response.ContentLength; responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false); var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData; @@ -475,18 +472,18 @@ namespace CryptoExchange.Net.Clients if (error.Code == null || error.Code == 0) error.Code = (int)response.StatusCode; - return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error!); + return new WebCallResult(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error!); } var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false); if (typeof(T) == typeof(object)) // Success status code and expected empty response, assume it's correct - return new WebCallResult(statusCode, headers, sw.Elapsed, 0, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]", request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null); + return new WebCallResult(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, 0, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]", request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null); if (!valid) { // Invalid json - return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, valid.Error); + return new WebCallResult(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, valid.Error); } // Json response received @@ -503,33 +500,55 @@ namespace CryptoExchange.Net.Clients } // Success status code, but TryParseError determined it was an error response - return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError); + return new WebCallResult(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError); } var deserializeResult = accessor.Deserialize(); - return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult.Data, deserializeResult.Error); + return new WebCallResult(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult.Data, deserializeResult.Error); } catch (HttpRequestException requestException) { // Request exception, can't reach server for instance var error = new WebError(requestException.Message, requestException); - return new WebCallResult(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error); + return new WebCallResult(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error); } catch (OperationCanceledException canceledException) { if (cancellationToken != default && canceledException.CancellationToken == cancellationToken) { // Cancellation token canceled by caller - return new WebCallResult(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError(canceledException)); + return new WebCallResult(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError(canceledException)); } else { // Request timed out var error = new WebError($"Request timed out", exception: canceledException); error.ErrorType = ErrorType.Timeout; - return new WebCallResult(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error); + return new WebCallResult(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error); } } + catch (ArgumentException argumentException) + { + if (argumentException.Message.StartsWith("Only HTTP/")) + { + // Unsupported HTTP version error .net framework + var error = ArgumentError.Invalid(nameof(RestExchangeOptions.HttpVersion), $"Invalid HTTP version {request.HttpVersion}: " + argumentException.Message); + return new WebCallResult(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error); + } + + throw; + } + catch (NotSupportedException notSupportedException) + { + if (notSupportedException.Message.StartsWith("Request version value must be one of")) + { + // Unsupported HTTP version error dotnet code + var error = ArgumentError.Invalid(nameof(RestExchangeOptions.HttpVersion), $"Invalid HTTP version {request.HttpVersion}: " + notSupportedException.Message); + return new WebCallResult(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error); + } + + throw; + } finally { accessor?.Clear(); @@ -674,21 +693,21 @@ namespace CryptoExchange.Net.Clients { base.SetOptions(options); - RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout); + RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout, ClientOptions.HttpKeepAliveInterval); } internal async Task> SyncTimeAsync() { var timeSyncParams = GetTimeSyncInfo(); if (timeSyncParams == null) - return new WebCallResult(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null); + return new WebCallResult(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null); if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false)) { if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval) { timeSyncParams.TimeSyncState.Semaphore.Release(); - return new WebCallResult(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null); + return new WebCallResult(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null); } var localTime = DateTime.UtcNow; @@ -717,7 +736,7 @@ namespace CryptoExchange.Net.Clients timeSyncParams.TimeSyncState.Semaphore.Release(); } - return new WebCallResult(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null); + return new WebCallResult(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null); } private bool ShouldCache(RequestDefinition definition) diff --git a/CryptoExchange.Net/Interfaces/IRequest.cs b/CryptoExchange.Net/Interfaces/IRequest.cs index 72ebe59..a80f65a 100644 --- a/CryptoExchange.Net/Interfaces/IRequest.cs +++ b/CryptoExchange.Net/Interfaces/IRequest.cs @@ -28,6 +28,10 @@ namespace CryptoExchange.Net.Interfaces /// Uri Uri { get; } /// + /// HTTP protocol version + /// + Version HttpVersion { get; } + /// /// internal request id for tracing /// int RequestId { get; } diff --git a/CryptoExchange.Net/Interfaces/IRequestFactory.cs b/CryptoExchange.Net/Interfaces/IRequestFactory.cs index f3cc827..ccb9233 100644 --- a/CryptoExchange.Net/Interfaces/IRequestFactory.cs +++ b/CryptoExchange.Net/Interfaces/IRequestFactory.cs @@ -1,4 +1,5 @@ using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Options; using System; using System.Net.Http; @@ -12,25 +13,21 @@ namespace CryptoExchange.Net.Interfaces /// /// Create a request for an uri /// - /// - /// - /// - /// - IRequest Create(HttpMethod method, Uri uri, int requestId); + IRequest Create(Version httpRequestVersion, HttpMethod method, Uri uri, int requestId); /// /// Configure the requests created by this factory /// - /// Request timeout to use + /// Rest client options /// Optional shared http client instance - /// Optional proxy to use when no http client is provided - void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient = null); + void Configure(RestExchangeOptions options, HttpClient? httpClient = null); /// /// Update settings /// /// Proxy to use /// Request timeout to use - void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout); + /// Http client keep alive interval + void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval); } } diff --git a/CryptoExchange.Net/Interfaces/IResponse.cs b/CryptoExchange.Net/Interfaces/IResponse.cs index 55f9921..cc853e7 100644 --- a/CryptoExchange.Net/Interfaces/IResponse.cs +++ b/CryptoExchange.Net/Interfaces/IResponse.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Net; using System.Threading.Tasks; @@ -15,6 +16,11 @@ namespace CryptoExchange.Net.Interfaces /// HttpStatusCode StatusCode { get; } + /// + /// Http protocol version + /// + Version HttpVersion { get; } + /// /// Whether the status code indicates a success status /// diff --git a/CryptoExchange.Net/LibraryHelpers.cs b/CryptoExchange.Net/LibraryHelpers.cs index f96fdc3..2a07dcb 100644 --- a/CryptoExchange.Net/LibraryHelpers.cs +++ b/CryptoExchange.Net/LibraryHelpers.cs @@ -1,5 +1,8 @@ -using System; +using CryptoExchange.Net.Objects; +using System; using System.Collections.Generic; +using System.Net; +using System.Net.Http; using System.Text; namespace CryptoExchange.Net @@ -43,5 +46,58 @@ namespace CryptoExchange.Net return clientOrderId; } + + /// + /// Create a new HttpMessageHandler instance + /// + public static HttpMessageHandler CreateHttpClientMessageHandler(ApiProxy? proxy, TimeSpan? keepAliveInterval) + { +#if NET5_0_OR_GREATER + var socketHandler = new SocketsHttpHandler(); + try + { + if (keepAliveInterval != null && keepAliveInterval != TimeSpan.Zero) + { + socketHandler.KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always; + socketHandler.KeepAlivePingDelay = keepAliveInterval.Value; + socketHandler.KeepAlivePingTimeout = TimeSpan.FromSeconds(10); + } + + socketHandler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; + socketHandler.DefaultProxyCredentials = CredentialCache.DefaultCredentials; + } + catch (PlatformNotSupportedException) { } + catch (NotImplementedException) { } // Mono runtime throws NotImplementedException + + if (proxy != null) + { + socketHandler.Proxy = new WebProxy + { + Address = new Uri($"{proxy.Host}:{proxy.Port}"), + Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password) + }; + } + return socketHandler; +#else + var httpHandler = new HttpClientHandler(); + try + { + httpHandler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; + httpHandler.DefaultProxyCredentials = CredentialCache.DefaultCredentials; + } + catch (PlatformNotSupportedException) { } + catch (NotImplementedException) { } // Mono runtime throws NotImplementedException + + if (proxy != null) + { + httpHandler.Proxy = new WebProxy + { + Address = new Uri($"{proxy.Host}:{proxy.Port}"), + Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password) + }; + } + return httpHandler; +#endif + } } } diff --git a/CryptoExchange.Net/Objects/CallResult.cs b/CryptoExchange.Net/Objects/CallResult.cs index e847bfc..7cf297c 100644 --- a/CryptoExchange.Net/Objects/CallResult.cs +++ b/CryptoExchange.Net/Objects/CallResult.cs @@ -205,6 +205,11 @@ namespace CryptoExchange.Net.Objects /// The request http method /// public HttpMethod? RequestMethod { get; set; } + + /// + /// HTTP protocol version + /// + public Version? HttpVersion { get; set; } /// /// The headers sent with the request @@ -251,6 +256,7 @@ namespace CryptoExchange.Net.Objects /// public WebCallResult( HttpStatusCode? code, + Version? httpVersion, KeyValuePair[]? responseHeaders, TimeSpan? responseTime, string? originalData, @@ -262,6 +268,7 @@ namespace CryptoExchange.Net.Objects Error? error) : base(error) { ResponseStatusCode = code; + HttpVersion = httpVersion; ResponseHeaders = responseHeaders; ResponseTime = responseTime; RequestId = requestId; @@ -286,7 +293,7 @@ namespace CryptoExchange.Net.Objects /// public WebCallResult AsError(Error error) { - return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error); + return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error); } /// @@ -297,7 +304,7 @@ namespace CryptoExchange.Net.Objects /// public WebCallResult As([AllowNull] K data) { - return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, data, Error); + return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, data, Error); } /// @@ -334,7 +341,7 @@ namespace CryptoExchange.Net.Objects /// public WebCallResult AsError(Error error) { - return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, default, error); + return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, default, error); } /// @@ -355,6 +362,11 @@ namespace CryptoExchange.Net.Objects /// public HttpMethod? RequestMethod { get; set; } + /// + /// HTTP protocol version + /// + public Version? HttpVersion { get; set; } + /// /// The headers sent with the request /// @@ -403,21 +415,9 @@ namespace CryptoExchange.Net.Objects /// /// Create a new result /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// public WebCallResult( HttpStatusCode? code, + Version? httpVersion, KeyValuePair[]? responseHeaders, TimeSpan? responseTime, long? responseLength, @@ -431,6 +431,7 @@ namespace CryptoExchange.Net.Objects [AllowNull] T data, Error? error) : base(data, originalData, error) { + HttpVersion = httpVersion; ResponseStatusCode = code; ResponseHeaders = responseHeaders; ResponseTime = responseTime; @@ -450,7 +451,7 @@ namespace CryptoExchange.Net.Objects /// public new WebCallResult AsDataless() { - return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error); + return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error); } /// /// Copy as a dataless result @@ -458,14 +459,14 @@ namespace CryptoExchange.Net.Objects /// public new WebCallResult AsDatalessError(Error error) { - return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error); + return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error); } /// /// Create a new error result /// /// The error - public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, default, error) { } + public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, default, error) { } /// /// Copy the WebCallResult to a new data type @@ -475,7 +476,7 @@ namespace CryptoExchange.Net.Objects /// public new WebCallResult As([AllowNull] K data) { - return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error); + return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error); } /// @@ -486,7 +487,7 @@ namespace CryptoExchange.Net.Objects /// public new WebCallResult AsError(Error error) { - return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, default, error); + return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, default, error); } /// @@ -498,7 +499,7 @@ namespace CryptoExchange.Net.Objects /// public new WebCallResult AsErrorWithData(Error error, K data) { - return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, error); + return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, error); } /// @@ -569,7 +570,7 @@ namespace CryptoExchange.Net.Objects /// internal WebCallResult Cached() { - return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Cache, Data, Error); + return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Cache, Data, Error); } /// diff --git a/CryptoExchange.Net/Objects/Options/RestExchangeOptions.cs b/CryptoExchange.Net/Objects/Options/RestExchangeOptions.cs index 6235782..a000383 100644 --- a/CryptoExchange.Net/Objects/Options/RestExchangeOptions.cs +++ b/CryptoExchange.Net/Objects/Options/RestExchangeOptions.cs @@ -1,5 +1,7 @@ using CryptoExchange.Net.Authentication; using System; +using System.Net; +using System.Net.Http; namespace CryptoExchange.Net.Objects.Options { @@ -28,6 +30,20 @@ namespace CryptoExchange.Net.Objects.Options /// public TimeSpan CachingMaxAge { get; set; } = TimeSpan.FromSeconds(5); + /// + /// The HTTP protocol version to use, typically 2.0 or 1.1 + /// + public Version HttpVersion { get; set; } +#if NET5_0_OR_GREATER + = new Version(2, 0); +#else + = new Version(1, 1); +#endif + /// + /// Http client keep alive interval for keeping connections open + /// + public TimeSpan? HttpKeepAliveInterval { get; set; } = TimeSpan.FromSeconds(15); + /// /// Set the values of this options on the target options /// @@ -43,6 +59,8 @@ namespace CryptoExchange.Net.Objects.Options item.RateLimitingBehaviour = RateLimitingBehaviour; item.CachingEnabled = CachingEnabled; item.CachingMaxAge = CachingMaxAge; + item.HttpVersion = HttpVersion; + item.HttpKeepAliveInterval = HttpKeepAliveInterval; return item; } } diff --git a/CryptoExchange.Net/Requests/Request.cs b/CryptoExchange.Net/Requests/Request.cs index 23f1f1c..f563253 100644 --- a/CryptoExchange.Net/Requests/Request.cs +++ b/CryptoExchange.Net/Requests/Request.cs @@ -50,6 +50,9 @@ namespace CryptoExchange.Net.Requests /// public Uri Uri => _request.RequestUri!; + /// + public Version HttpVersion => _request.Version!; + /// public int RequestId { get; } @@ -81,7 +84,9 @@ namespace CryptoExchange.Net.Requests /// public async Task GetResponseAsync(CancellationToken cancellationToken) { - return new Response(await _httpClient.SendAsync(_request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false)); + var response = await _httpClient.SendAsync(_request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + + return new Response(response); } } } diff --git a/CryptoExchange.Net/Requests/RequestFactory.cs b/CryptoExchange.Net/Requests/RequestFactory.cs index 83a5dc7..7523d18 100644 --- a/CryptoExchange.Net/Requests/RequestFactory.cs +++ b/CryptoExchange.Net/Requests/RequestFactory.cs @@ -3,6 +3,7 @@ using System.Net; using System.Net.Http; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Options; namespace CryptoExchange.Net.Requests { @@ -14,54 +15,43 @@ namespace CryptoExchange.Net.Requests private HttpClient? _httpClient; /// - public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null) + public void Configure(RestExchangeOptions options, HttpClient? client = null) { if (client == null) - client = CreateClient(proxy, requestTimeout); + client = CreateClient(options.Proxy, options.RequestTimeout, options.HttpKeepAliveInterval); _httpClient = client; } /// - public IRequest Create(HttpMethod method, Uri uri, int requestId) + public IRequest Create(Version httpRequestVersion, HttpMethod method, Uri uri, int requestId) { if (_httpClient == null) throw new InvalidOperationException("Cant create request before configuring http client"); - return new Request(new HttpRequestMessage(method, uri), _httpClient, requestId); + var requestMessage = new HttpRequestMessage(method, uri); + requestMessage.Version = httpRequestVersion; +#if NET5_0_OR_GREATER + requestMessage.VersionPolicy = HttpVersionPolicy.RequestVersionOrLower; +#endif + return new Request(requestMessage, _httpClient, requestId); } /// - public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout) + public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval) { - _httpClient = CreateClient(proxy, requestTimeout); + _httpClient = CreateClient(proxy, requestTimeout, httpKeepAliveInterval); } - private static HttpClient CreateClient(ApiProxy? proxy, TimeSpan requestTimeout) + private static HttpClient CreateClient(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval) { - var handler = new HttpClientHandler(); - try - { - handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; - handler.DefaultProxyCredentials = CredentialCache.DefaultCredentials; - } - catch (PlatformNotSupportedException) { } - catch (NotImplementedException) { } // Mono runtime throws NotImplementedException - - if (proxy != null) - { - handler.Proxy = new WebProxy - { - Address = new Uri($"{proxy.Host}:{proxy.Port}"), - Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password) - }; - } - + var handler = LibraryHelpers.CreateHttpClientMessageHandler(proxy, httpKeepAliveInterval); var client = new HttpClient(handler) { - Timeout = requestTimeout + Timeout = requestTimeout }; return client; } + } } diff --git a/CryptoExchange.Net/Requests/Response.cs b/CryptoExchange.Net/Requests/Response.cs index 78505b1..f620d14 100644 --- a/CryptoExchange.Net/Requests/Response.cs +++ b/CryptoExchange.Net/Requests/Response.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; @@ -18,6 +19,9 @@ namespace CryptoExchange.Net.Requests /// public HttpStatusCode StatusCode => _response.StatusCode; + /// + public Version HttpVersion => _response.Version; + /// public bool IsSuccessStatusCode => _response.IsSuccessStatusCode; diff --git a/CryptoExchange.Net/SharedApis/Models/ExchangeWebResult.cs b/CryptoExchange.Net/SharedApis/Models/ExchangeWebResult.cs index b00ad20..f3f7ea0 100644 --- a/CryptoExchange.Net/SharedApis/Models/ExchangeWebResult.cs +++ b/CryptoExchange.Net/SharedApis/Models/ExchangeWebResult.cs @@ -48,6 +48,7 @@ namespace CryptoExchange.Net.SharedApis WebCallResult result, INextPageToken? nextPageToken = null) : base(result.ResponseStatusCode, + result.HttpVersion, result.ResponseHeaders, result.ResponseTime, result.ResponseLength, @@ -75,6 +76,7 @@ namespace CryptoExchange.Net.SharedApis WebCallResult result, INextPageToken? nextPageToken = null) : base(result.ResponseStatusCode, + result.HttpVersion, result.ResponseHeaders, result.ResponseTime, result.ResponseLength, @@ -100,6 +102,7 @@ namespace CryptoExchange.Net.SharedApis string exchange, TradingMode[]? dataTradeModes, HttpStatusCode? code, + Version? httpVersion, KeyValuePair[]? responseHeaders, TimeSpan? responseTime, long? responseLength, @@ -114,6 +117,7 @@ namespace CryptoExchange.Net.SharedApis Error? error, INextPageToken? nextPageToken = null) : base( code, + httpVersion, responseHeaders, responseTime, responseLength, @@ -140,7 +144,7 @@ namespace CryptoExchange.Net.SharedApis /// public new ExchangeWebResult As([AllowNull] K data) { - return new ExchangeWebResult(Exchange, DataTradeMode, ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error, NextPageToken); + return new ExchangeWebResult(Exchange, DataTradeMode, ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error, NextPageToken); } /// diff --git a/CryptoExchange.Net/Testing/Implementations/TestRequest.cs b/CryptoExchange.Net/Testing/Implementations/TestRequest.cs index dc6ba5a..ce5f07e 100644 --- a/CryptoExchange.Net/Testing/Implementations/TestRequest.cs +++ b/CryptoExchange.Net/Testing/Implementations/TestRequest.cs @@ -21,6 +21,8 @@ namespace CryptoExchange.Net.Testing.Implementations public Uri Uri { get; set; } + public Version HttpVersion { get; set; } + public int RequestId { get; set; } #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. diff --git a/CryptoExchange.Net/Testing/Implementations/TestRequestFactory.cs b/CryptoExchange.Net/Testing/Implementations/TestRequestFactory.cs index 293c2ea..5d3c7b7 100644 --- a/CryptoExchange.Net/Testing/Implementations/TestRequestFactory.cs +++ b/CryptoExchange.Net/Testing/Implementations/TestRequestFactory.cs @@ -1,5 +1,6 @@ using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Options; using System; using System.Net.Http; @@ -14,11 +15,11 @@ namespace CryptoExchange.Net.Testing.Implementations _request = request; } - public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient = null) - { + public void Configure(RestExchangeOptions options, HttpClient? client) + { } - public IRequest Create(HttpMethod method, Uri uri, int requestId) + public IRequest Create(Version httpRequestVersion, HttpMethod method, Uri uri, int requestId) { _request.Method = method; _request.Uri = uri; @@ -26,6 +27,6 @@ namespace CryptoExchange.Net.Testing.Implementations return _request; } - public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout) {} + public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval) {} } } diff --git a/CryptoExchange.Net/Testing/Implementations/TestResponse.cs b/CryptoExchange.Net/Testing/Implementations/TestResponse.cs index 53d59e4..ced090b 100644 --- a/CryptoExchange.Net/Testing/Implementations/TestResponse.cs +++ b/CryptoExchange.Net/Testing/Implementations/TestResponse.cs @@ -1,4 +1,5 @@ using CryptoExchange.Net.Interfaces; +using System; using System.Collections.Generic; using System.IO; using System.Net; @@ -11,6 +12,7 @@ namespace CryptoExchange.Net.Testing.Implementations private readonly Stream _response; public HttpStatusCode StatusCode { get; } + public Version HttpVersion { get; } public bool IsSuccessStatusCode { get; } @@ -21,6 +23,7 @@ namespace CryptoExchange.Net.Testing.Implementations public TestResponse(HttpStatusCode code, Stream response) { StatusCode = code; + HttpVersion = new Version(2, 0); IsSuccessStatusCode = code == HttpStatusCode.OK; _response = response; }