Skip to content

Commit 518dc0b

Browse files
authored
fix(request-content-type): use provided content type in case of null body (#75)
1 parent f8c2005 commit 518dc0b

File tree

5 files changed

+110
-33
lines changed

5 files changed

+110
-33
lines changed

APIMatic.Core.Test/Api/HttpPost/ApiCallPostTest.cs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
using System;
2-
using System.Collections.Generic;
2+
using System.Collections.Generic;
33
using System.Net;
44
using System.Net.Http;
55
using System.Net.Http.Json;
@@ -494,5 +494,31 @@ public void ApiCall_PostNullHeaderValue_OKResponse()
494494
Assert.NotNull(actual.Data);
495495
Assert.AreEqual(actual.Data.Message, expected.Message);
496496
}
497+
498+
[Test]
499+
public void ApiCall_PostNullBodyValue_OKResponse()
500+
{
501+
//Arrange
502+
const string url = "/apicall/null-body-post/200";
503+
504+
handlerMock.When(GetCompleteUrl(url))
505+
.With(req =>
506+
{
507+
Assert.AreEqual("application/json", req.Content?.Headers.ContentType?.MediaType);
508+
return true;
509+
})
510+
.Respond(HttpStatusCode.OK);
511+
512+
var apiCall = CreateApiCall<ServerResponse>()
513+
.RequestBuilder(requestBuilderAction => requestBuilderAction
514+
.Setup(HttpMethod.Post, url)
515+
.Parameters(p => p
516+
.Body(b => b.Setup(null))
517+
.Header(h => h.Setup("content-type", "application/json"))))
518+
.ExecuteAsync();
519+
520+
// Act
521+
CoreHelper.RunTask(apiCall);
522+
}
497523
}
498524
}

APIMatic.Core/Http/HttpClientWrapper.cs

Lines changed: 41 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using System.Threading.Tasks;
1515
using APIMatic.Core.Http.Configuration;
1616
using APIMatic.Core.Types.Sdk;
17+
using APIMatic.Core.Utilities;
1718
using Polly;
1819
using Polly.Retry;
1920
using Polly.Timeout;
@@ -104,38 +105,19 @@ private HttpRequestMessage CreateHttpRequestMessageFromRequest(CoreRequest reque
104105
HttpRequestMessage requestMessage = new HttpRequestMessage
105106
{
106107
RequestUri = new Uri(request.QueryUrl),
107-
Method = request.HttpMethod,
108+
Method = request.HttpMethod
108109
};
109110

110-
if (request.Headers != null)
111-
{
112-
foreach (var headers in request.Headers)
113-
{
114-
requestMessage.Headers.TryAddWithoutValidation(headers.Key, headers.Value);
115-
}
116-
}
111+
AddHeadersToRequestMessage(requestMessage, request);
112+
if (IsHeaderOnlyHttpMethod(request.HttpMethod)) return requestMessage;
117113

118-
if (IsHeaderOnlyHttpMethod(request.HttpMethod))
114+
if (request.HasFormParameters)
119115
{
116+
requestMessage.Content = GetFormContent(request);
120117
return requestMessage;
121118
}
122119

123-
if (request.Body == null)
124-
{
125-
if (CheckFormParametersForMultiPart(request.FormParameters))
126-
{
127-
requestMessage.Content = GetMultipartFormDataContentFromRequest(request);
128-
return requestMessage;
129-
}
130-
131-
requestMessage.Content = new FormUrlEncodedContent(request.FormParameters.Select(param => new KeyValuePair<string, string>(param.Key, param.Value.ToString())).ToList());
132-
return requestMessage;
133-
}
134-
135-
string contentType = request.Headers?.Where(p => p.Key.Equals("content-type", StringComparison.InvariantCultureIgnoreCase))
136-
.Select(x => x.Value)
137-
.FirstOrDefault();
138-
120+
var contentType = request.GetContentType();
139121
if (request.Body is CoreFileStreamInfo file)
140122
{
141123
file.FileStream.Position = 0;
@@ -146,21 +128,50 @@ private HttpRequestMessage CreateHttpRequestMessageFromRequest(CoreRequest reque
146128

147129
if (string.IsNullOrEmpty(contentType))
148130
{
149-
requestMessage.Content = new StringContent(request.Body.ToString(), Encoding.UTF8, "text/plain");
131+
requestMessage.Content =
132+
new StringContent(request.GetBodyAsString(), Encoding.UTF8, "text/plain");
150133
return requestMessage;
151134
}
152135

153-
if (contentType.Equals("application/json; charset=utf-8", StringComparison.OrdinalIgnoreCase))
136+
if (IsContentTypeJsonUtf8(contentType))
154137
{
155-
requestMessage.Content = new StringContent(request.Body.ToString(), Encoding.UTF8, "application/json");
138+
requestMessage.Content =
139+
new StringContent(request.GetBodyAsString(), Encoding.UTF8, "application/json");
156140
return requestMessage;
157141
}
158142

143+
if (request.Body == null) return requestMessage;
144+
159145
requestMessage.Content = GetByteArrayContentFromRequestBody(request.Body);
160146
GetByteArrayContentType(requestMessage.Content.Headers, contentType);
161147
return requestMessage;
162148
}
149+
150+
private static void AddHeadersToRequestMessage(HttpRequestMessage requestMessage, CoreRequest request)
151+
{
152+
foreach (var headers in request.Headers ?? Enumerable.Empty<KeyValuePair<string, string>>())
153+
{
154+
requestMessage.Headers.TryAddWithoutValidation(headers.Key, headers.Value);
155+
}
156+
}
163157

158+
private static HttpContent GetFormContent(CoreRequest request)
159+
{
160+
if (CheckFormParametersForMultiPart(request.FormParameters))
161+
{
162+
return GetMultipartFormDataContentFromRequest(request);
163+
}
164+
165+
return new FormUrlEncodedContent(request.FormParameters
166+
.Select(param => new KeyValuePair<string, string>(param.Key, param.Value.ToString())).ToList());
167+
}
168+
169+
private static bool IsContentTypeJsonUtf8(string contentType)
170+
{
171+
return contentType.EqualsIgnoreCase("application/json") ||
172+
contentType.EqualsIgnoreCase("application/json; charset=utf-8");
173+
}
174+
164175
private static void GetByteArrayContentType(HttpContentHeaders contentHeader, string contentType)
165176
{
166177
try
@@ -239,9 +250,8 @@ private static bool IsHeaderOnlyHttpMethod(HttpMethod method)
239250

240251
private static bool CheckFormParametersForMultiPart(IReadOnlyCollection<KeyValuePair<string, object>> formParameters)
241252
{
242-
return formParameters != null &&
243-
(formParameters.Any(f => f.Value is MultipartContent) ||
244-
formParameters.Any(f => f.Value is CoreFileStreamInfo));
253+
return formParameters.Any(f => f.Value is MultipartContent) ||
254+
formParameters.Any(f => f.Value is CoreFileStreamInfo);
245255
}
246256

247257
private bool ShouldRetry(HttpResponseMessage response, RetryOption retryOption)

APIMatic.Core/Request/RequestBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ private bool ContentHeaderKeyRequired(string key)
209209
{
210210
return false;
211211
}
212-
if (headers.Any(p => p.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)))
212+
if (headers.Any(p => p.Key.EqualsIgnoreCase(key)))
213213
{
214214
return false;
215215
}

APIMatic.Core/Types/Sdk/CoreRequest.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,13 @@ public CoreRequest(HttpMethod method, string queryUrl, Dictionary<string, string
8181
/// </summary>
8282
internal bool HasBinaryResponse { get; set; }
8383

84+
/// <summary>
85+
/// Gets a value indicating whether the request is likely to have form content.
86+
/// This is determined by checking if the <see cref="Body"/> is null
87+
/// and if the <see cref="FormParameters"/> collection is not null and contains any items.
88+
/// </summary>
89+
internal bool HasFormParameters => Body == null && FormParameters != null && FormParameters.Any();
90+
8491
/// <summary>
8592
/// Concatenate values from a Dictionary to this object.
8693
/// </summary>
@@ -103,6 +110,25 @@ public void AddQueryParameters(Dictionary<string, object> queryParamaters)
103110
?? new Dictionary<string, object>(queryParamaters);
104111
}
105112

113+
/// <summary>
114+
/// Retrieves the value of the "Content-Type" header from the request headers.
115+
/// </summary>
116+
/// <returns>
117+
/// The value of the "Content-Type" header if present; otherwise, <c>null</c>.
118+
/// </returns>
119+
internal string GetContentType() => Headers?.Where(p => p.Key.EqualsIgnoreCase("content-type"))
120+
.Select(x => x.Value)
121+
.FirstOrDefault();
122+
123+
/// <summary>
124+
/// Converts the body of the request to a string representation.
125+
/// </summary>
126+
/// <returns>
127+
/// The string representation of the <see cref="Body"/> if it is not <c>null</c>;
128+
/// otherwise, returns an empty string.
129+
/// </returns>
130+
internal string GetBodyAsString() => Body == null ? string.Empty : Body.ToString();
131+
106132
/// <inheritdoc/>
107133
public override string ToString()
108134
{
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// <copyright file="StringExtensions.cs" company="APIMatic">
2+
// Copyright (c) APIMatic. All rights reserved.
3+
// </copyright>
4+
using System;
5+
6+
namespace APIMatic.Core.Utilities
7+
{
8+
internal static class StringExtensions
9+
{
10+
public static bool EqualsIgnoreCase(this string source, string target)
11+
{
12+
return source.Equals(target, StringComparison.InvariantCultureIgnoreCase);
13+
}
14+
}
15+
}

0 commit comments

Comments
 (0)