diff --git a/APIMatic.Core.Test/Api/HttpPost/ApiCallPostTest.cs b/APIMatic.Core.Test/Api/HttpPost/ApiCallPostTest.cs index 069ad66..32e94bf 100644 --- a/APIMatic.Core.Test/Api/HttpPost/ApiCallPostTest.cs +++ b/APIMatic.Core.Test/Api/HttpPost/ApiCallPostTest.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Net.Http.Json; @@ -494,5 +494,31 @@ public void ApiCall_PostNullHeaderValue_OKResponse() Assert.NotNull(actual.Data); Assert.AreEqual(actual.Data.Message, expected.Message); } + + [Test] + public void ApiCall_PostNullBodyValue_OKResponse() + { + //Arrange + const string url = "/apicall/null-body-post/200"; + + handlerMock.When(GetCompleteUrl(url)) + .With(req => + { + Assert.AreEqual("application/json", req.Content?.Headers.ContentType?.MediaType); + return true; + }) + .Respond(HttpStatusCode.OK); + + var apiCall = CreateApiCall() + .RequestBuilder(requestBuilderAction => requestBuilderAction + .Setup(HttpMethod.Post, url) + .Parameters(p => p + .Body(b => b.Setup(null)) + .Header(h => h.Setup("content-type", "application/json")))) + .ExecuteAsync(); + + // Act + CoreHelper.RunTask(apiCall); + } } } diff --git a/APIMatic.Core/Http/HttpClientWrapper.cs b/APIMatic.Core/Http/HttpClientWrapper.cs index 8960f91..cfc6a99 100644 --- a/APIMatic.Core/Http/HttpClientWrapper.cs +++ b/APIMatic.Core/Http/HttpClientWrapper.cs @@ -14,6 +14,7 @@ using System.Threading.Tasks; using APIMatic.Core.Http.Configuration; using APIMatic.Core.Types.Sdk; +using APIMatic.Core.Utilities; using Polly; using Polly.Retry; using Polly.Timeout; @@ -104,38 +105,19 @@ private HttpRequestMessage CreateHttpRequestMessageFromRequest(CoreRequest reque HttpRequestMessage requestMessage = new HttpRequestMessage { RequestUri = new Uri(request.QueryUrl), - Method = request.HttpMethod, + Method = request.HttpMethod }; - if (request.Headers != null) - { - foreach (var headers in request.Headers) - { - requestMessage.Headers.TryAddWithoutValidation(headers.Key, headers.Value); - } - } + AddHeadersToRequestMessage(requestMessage, request); + if (IsHeaderOnlyHttpMethod(request.HttpMethod)) return requestMessage; - if (IsHeaderOnlyHttpMethod(request.HttpMethod)) + if (request.HasFormParameters) { + requestMessage.Content = GetFormContent(request); return requestMessage; } - if (request.Body == null) - { - if (CheckFormParametersForMultiPart(request.FormParameters)) - { - requestMessage.Content = GetMultipartFormDataContentFromRequest(request); - return requestMessage; - } - - requestMessage.Content = new FormUrlEncodedContent(request.FormParameters.Select(param => new KeyValuePair(param.Key, param.Value.ToString())).ToList()); - return requestMessage; - } - - string contentType = request.Headers?.Where(p => p.Key.Equals("content-type", StringComparison.InvariantCultureIgnoreCase)) - .Select(x => x.Value) - .FirstOrDefault(); - + var contentType = request.GetContentType(); if (request.Body is CoreFileStreamInfo file) { file.FileStream.Position = 0; @@ -146,21 +128,50 @@ private HttpRequestMessage CreateHttpRequestMessageFromRequest(CoreRequest reque if (string.IsNullOrEmpty(contentType)) { - requestMessage.Content = new StringContent(request.Body.ToString(), Encoding.UTF8, "text/plain"); + requestMessage.Content = + new StringContent(request.GetBodyAsString(), Encoding.UTF8, "text/plain"); return requestMessage; } - if (contentType.Equals("application/json; charset=utf-8", StringComparison.OrdinalIgnoreCase)) + if (IsContentTypeJsonUtf8(contentType)) { - requestMessage.Content = new StringContent(request.Body.ToString(), Encoding.UTF8, "application/json"); + requestMessage.Content = + new StringContent(request.GetBodyAsString(), Encoding.UTF8, "application/json"); return requestMessage; } + if (request.Body == null) return requestMessage; + requestMessage.Content = GetByteArrayContentFromRequestBody(request.Body); GetByteArrayContentType(requestMessage.Content.Headers, contentType); return requestMessage; } + + private static void AddHeadersToRequestMessage(HttpRequestMessage requestMessage, CoreRequest request) + { + foreach (var headers in request.Headers ?? Enumerable.Empty>()) + { + requestMessage.Headers.TryAddWithoutValidation(headers.Key, headers.Value); + } + } + private static HttpContent GetFormContent(CoreRequest request) + { + if (CheckFormParametersForMultiPart(request.FormParameters)) + { + return GetMultipartFormDataContentFromRequest(request); + } + + return new FormUrlEncodedContent(request.FormParameters + .Select(param => new KeyValuePair(param.Key, param.Value.ToString())).ToList()); + } + + private static bool IsContentTypeJsonUtf8(string contentType) + { + return contentType.EqualsIgnoreCase("application/json") || + contentType.EqualsIgnoreCase("application/json; charset=utf-8"); + } + private static void GetByteArrayContentType(HttpContentHeaders contentHeader, string contentType) { try @@ -239,9 +250,8 @@ private static bool IsHeaderOnlyHttpMethod(HttpMethod method) private static bool CheckFormParametersForMultiPart(IReadOnlyCollection> formParameters) { - return formParameters != null && - (formParameters.Any(f => f.Value is MultipartContent) || - formParameters.Any(f => f.Value is CoreFileStreamInfo)); + return formParameters.Any(f => f.Value is MultipartContent) || + formParameters.Any(f => f.Value is CoreFileStreamInfo); } private bool ShouldRetry(HttpResponseMessage response, RetryOption retryOption) diff --git a/APIMatic.Core/Request/RequestBuilder.cs b/APIMatic.Core/Request/RequestBuilder.cs index 3e991b0..6f394ce 100644 --- a/APIMatic.Core/Request/RequestBuilder.cs +++ b/APIMatic.Core/Request/RequestBuilder.cs @@ -209,7 +209,7 @@ private bool ContentHeaderKeyRequired(string key) { return false; } - if (headers.Any(p => p.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase))) + if (headers.Any(p => p.Key.EqualsIgnoreCase(key))) { return false; } diff --git a/APIMatic.Core/Types/Sdk/CoreRequest.cs b/APIMatic.Core/Types/Sdk/CoreRequest.cs index 2faaab7..e0ceb76 100644 --- a/APIMatic.Core/Types/Sdk/CoreRequest.cs +++ b/APIMatic.Core/Types/Sdk/CoreRequest.cs @@ -81,6 +81,13 @@ public CoreRequest(HttpMethod method, string queryUrl, Dictionary internal bool HasBinaryResponse { get; set; } + /// + /// Gets a value indicating whether the request is likely to have form content. + /// This is determined by checking if the is null + /// and if the collection is not null and contains any items. + /// + internal bool HasFormParameters => Body == null && FormParameters != null && FormParameters.Any(); + /// /// Concatenate values from a Dictionary to this object. /// @@ -103,6 +110,25 @@ public void AddQueryParameters(Dictionary queryParamaters) ?? new Dictionary(queryParamaters); } + /// + /// Retrieves the value of the "Content-Type" header from the request headers. + /// + /// + /// The value of the "Content-Type" header if present; otherwise, null. + /// + internal string GetContentType() => Headers?.Where(p => p.Key.EqualsIgnoreCase("content-type")) + .Select(x => x.Value) + .FirstOrDefault(); + + /// + /// Converts the body of the request to a string representation. + /// + /// + /// The string representation of the if it is not null; + /// otherwise, returns an empty string. + /// + internal string GetBodyAsString() => Body == null ? string.Empty : Body.ToString(); + /// public override string ToString() { diff --git a/APIMatic.Core/Utilities/StringExtensions.cs b/APIMatic.Core/Utilities/StringExtensions.cs new file mode 100644 index 0000000..75f5ed8 --- /dev/null +++ b/APIMatic.Core/Utilities/StringExtensions.cs @@ -0,0 +1,15 @@ +// +// Copyright (c) APIMatic. All rights reserved. +// +using System; + +namespace APIMatic.Core.Utilities +{ + internal static class StringExtensions + { + public static bool EqualsIgnoreCase(this string source, string target) + { + return source.Equals(target, StringComparison.InvariantCultureIgnoreCase); + } + } +}