diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs index 7c7d6f7..7c37fb9 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs @@ -56,7 +56,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations var request = new Mock(); request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com")); request.Setup(c => c.GetResponseAsync(It.IsAny())).Returns(Task.FromResult(response.Object)); - request.Setup(c => c.SetContent(It.IsAny(), It.IsAny())).Callback(new Action((content, type) => { request.Setup(r => r.Content).Returns(content); })); + request.Setup(c => c.SetContent(It.IsAny(), It.IsAny(), It.IsAny())).Callback(new Action((content, encoding, type) => { request.Setup(r => r.Content).Returns(content); })); request.Setup(c => c.AddHeader(It.IsAny(), It.IsAny())).Callback((key, val) => headers.Add(key, new string[] { val })); request.Setup(c => c.GetHeaders()).Returns(() => headers); diff --git a/CryptoExchange.Net/Clients/RestApiClient.cs b/CryptoExchange.Net/Clients/RestApiClient.cs index 032ab5f..45b4b7d 100644 --- a/CryptoExchange.Net/Clients/RestApiClient.cs +++ b/CryptoExchange.Net/Clients/RestApiClient.cs @@ -77,6 +77,16 @@ namespace CryptoExchange.Net.Clients { new HttpMethod("Patch"), HttpMethodParameterPosition.InBody }, }; + /// + /// Encoding/charset for the ContentType header + /// + protected Encoding? RequestBodyContentEncoding { get; set; } = Encoding.UTF8; + + /// + /// Whether to omit the ContentType header if there is no content + /// + protected bool OmitContentTypeHeaderWithoutContent { get; set; } = false; + /// public new RestExchangeOptions ClientOptions => (RestExchangeOptions)base.ClientOptions; @@ -374,7 +384,11 @@ namespace CryptoExchange.Net.Clients if (!string.IsNullOrEmpty(queryString) && !queryString.StartsWith("?")) queryString = $"?{queryString}"; - var uri = new Uri(baseAddress.AppendPath(definition.Path) + queryString); + var path = baseAddress.AppendPath(definition.Path); + if (definition.ForcePathEndWithSlash == true && !path.EndsWith("/")) + path += "/"; + + var uri = new Uri(path + queryString); var request = RequestFactory.Create(ClientOptions.HttpVersion, definition.Method, uri, requestId); request.Accept = MessageHandler.AcceptHeader; @@ -398,14 +412,14 @@ namespace CryptoExchange.Net.Clients var bodyContent = requestConfiguration.GetBodyContent(); if (bodyContent != null) { - request.SetContent(bodyContent, contentType); + request.SetContent(bodyContent, RequestBodyContentEncoding, contentType); } else { if (requestConfiguration.BodyParameters != null && requestConfiguration.BodyParameters.Count != 0) WriteParamBody(request, requestConfiguration.BodyParameters, contentType); - else - request.SetContent(RequestBodyEmptyContent, contentType); + else if (OmitContentTypeHeaderWithoutContent != true) + request.SetContent(RequestBodyEmptyContent, RequestBodyContentEncoding, contentType); } } @@ -646,13 +660,13 @@ namespace CryptoExchange.Net.Clients stringData = stringSerializer.Serialize(value); else stringData = stringSerializer.Serialize(parameters); - request.SetContent(stringData, contentType); + request.SetContent(stringData, RequestBodyContentEncoding, contentType); } else if (contentType == Constants.FormContentHeader) { // Write the parameters as form data in the body var stringData = parameters.ToFormData(); - request.SetContent(stringData, contentType); + request.SetContent(stringData, RequestBodyContentEncoding, contentType); } } diff --git a/CryptoExchange.Net/Interfaces/IRequest.cs b/CryptoExchange.Net/Interfaces/IRequest.cs index 739593a..7ec4507 100644 --- a/CryptoExchange.Net/Interfaces/IRequest.cs +++ b/CryptoExchange.Net/Interfaces/IRequest.cs @@ -1,6 +1,7 @@ using System; using System.Net.Http; using System.Net.Http.Headers; +using System.Text; using System.Threading; using System.Threading.Tasks; @@ -43,9 +44,7 @@ namespace CryptoExchange.Net.Interfaces /// /// Set string content /// - /// - /// - void SetContent(string data, string contentType); + void SetContent(string data, Encoding? encoding, string contentType); /// /// Add a header to the request diff --git a/CryptoExchange.Net/Objects/RequestDefinition.cs b/CryptoExchange.Net/Objects/RequestDefinition.cs index 429100d..0aaec7e 100644 --- a/CryptoExchange.Net/Objects/RequestDefinition.cs +++ b/CryptoExchange.Net/Objects/RequestDefinition.cs @@ -72,6 +72,11 @@ namespace CryptoExchange.Net.Objects /// public int? ConnectionId { get; set; } + /// + /// Whether the endpoint path should always include the trailing `/` + /// + public bool? ForcePathEndWithSlash { get; set; } + /// /// ctor /// diff --git a/CryptoExchange.Net/Objects/RequestDefinitionCache.cs b/CryptoExchange.Net/Objects/RequestDefinitionCache.cs index 1fac34b..2835477 100644 --- a/CryptoExchange.Net/Objects/RequestDefinitionCache.cs +++ b/CryptoExchange.Net/Objects/RequestDefinitionCache.cs @@ -47,6 +47,7 @@ namespace CryptoExchange.Net.Objects /// Array serialization type /// Prevent request caching /// Try parse the response even when status is not success + /// Force trailing `/` /// public RequestDefinition GetOrCreate( HttpMethod method, @@ -59,8 +60,9 @@ namespace CryptoExchange.Net.Objects HttpMethodParameterPosition? parameterPosition = null, ArrayParametersSerialization? arraySerialization = null, bool? preventCaching = null, - bool? tryParseOnNonSuccess = null) - => GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching, tryParseOnNonSuccess); + bool? tryParseOnNonSuccess = null, + bool? forcePathEndWithSlash = null) + => GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching, tryParseOnNonSuccess, forcePathEndWithSlash); /// /// Get a definition if it is already in the cache or create a new definition and add it to the cache @@ -77,6 +79,7 @@ namespace CryptoExchange.Net.Objects /// Array serialization type /// Prevent request caching /// Try parse the response even when status is not success + /// Force trailing `/` /// public RequestDefinition GetOrCreate( string identifier, @@ -90,7 +93,8 @@ namespace CryptoExchange.Net.Objects HttpMethodParameterPosition? parameterPosition = null, ArrayParametersSerialization? arraySerialization = null, bool? preventCaching = null, - bool? tryParseOnNonSuccess = null) + bool? tryParseOnNonSuccess = null, + bool? forcePathEndWithSlash = null) { if (!_definitions.TryGetValue(identifier, out var def)) @@ -105,7 +109,8 @@ namespace CryptoExchange.Net.Objects RequestBodyFormat = requestBodyFormat, ParameterPosition = parameterPosition, PreventCaching = preventCaching ?? false, - TryParseOnNonSuccess = tryParseOnNonSuccess ?? false + TryParseOnNonSuccess = tryParseOnNonSuccess ?? false, + ForcePathEndWithSlash = forcePathEndWithSlash ?? false }; _definitions.TryAdd(identifier, def); } diff --git a/CryptoExchange.Net/Requests/Request.cs b/CryptoExchange.Net/Requests/Request.cs index 698db12..98496e2 100644 --- a/CryptoExchange.Net/Requests/Request.cs +++ b/CryptoExchange.Net/Requests/Request.cs @@ -19,9 +19,6 @@ namespace CryptoExchange.Net.Requests /// /// Create request object for web request /// - /// - /// - /// public Request(HttpRequestMessage request, HttpClient client, int requestId) { _httpClient = client; @@ -55,10 +52,12 @@ namespace CryptoExchange.Net.Requests public int RequestId { get; } /// - public void SetContent(string data, string contentType) + public void SetContent(string data, Encoding? encoding, string contentType) { Content = data; - _request.Content = new StringContent(data, Encoding.UTF8, contentType); + _request.Content = new StringContent(data, encoding ?? Encoding.UTF8, contentType); + if (encoding == null) + _request.Content.Headers.ContentType!.CharSet = null; } /// diff --git a/CryptoExchange.Net/Testing/Implementations/TestRequest.cs b/CryptoExchange.Net/Testing/Implementations/TestRequest.cs index 1cb84ec..b7a77bf 100644 --- a/CryptoExchange.Net/Testing/Implementations/TestRequest.cs +++ b/CryptoExchange.Net/Testing/Implementations/TestRequest.cs @@ -46,7 +46,7 @@ namespace CryptoExchange.Net.Testing.Implementations Content = Encoding.UTF8.GetString(data); } - public void SetContent(string data, string contentType) + public void SetContent(string data, Encoding? encoding, string contentType) { Content = data; }