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; }