diff --git a/Frends.HTTP.Request/CHANGELOG.md b/Frends.HTTP.Request/CHANGELOG.md
index c349f64..2848713 100644
--- a/Frends.HTTP.Request/CHANGELOG.md
+++ b/Frends.HTTP.Request/CHANGELOG.md
@@ -1,53 +1,88 @@
# Changelog
+## [1.8.0] - 2026-03-07
+
+### Fixed
+
+- Improve the handling of disposable objects to avoid problems with timeouts related to HttpClient.
+
+### Added
+
+- Added an option "CacheHttpClient" to disable caching httpClient. (there were cached by default)
+
## [1.7.0] - 2026-03-03
+
### Added
-- Added CertificateStoreLocation option to allow selection between CurrentUser and LocalMachine certificate stores when using certificate authentication.
+
+- Added CertificateStoreLocation option to allow selection between CurrentUser and LocalMachine certificate stores when
+ using certificate authentication.
## [1.6.0] - 2026-01-27
+
### Fixed
+
- GET requests ignore message body content
## [1.5.0] - 2025-10-03
+
### Changed
+
- Changed default return format from String to JToken
## [1.4.0] - 2025-03-25
+
### Changed
+
- Update packages:
- Newtonsoft.Json 12.0.1 -> 13.0.3
- System.DirectoryServices 7.0.0 -> 9.0.3
- System.Runtime.Caching 7.0.0 -> 9.0.3
- coverlet.collector 3.1.0 -> 6.0.4
- Microsoft.NET.Test.Sdk 16.7.0 -> 17.13.0
- MSTest.TestAdapter 2.1.2 -> 3.8.3
- MSTest.TestFramework 2.1.2 -> 3.8.3
- nunit 3.12.0 -> 4.3.2
- NUnit3TestAdapter 3.17.0 -> 5.0.0
- RichardSzalay.MockHttp 6.0.0 -> 7.0.0
- xunit.extensibility.core 2.4.2 -> 2.9.3
+ Newtonsoft.Json 12.0.1 -> 13.0.3
+ System.DirectoryServices 7.0.0 -> 9.0.3
+ System.Runtime.Caching 7.0.0 -> 9.0.3
+ coverlet.collector 3.1.0 -> 6.0.4
+ Microsoft.NET.Test.Sdk 16.7.0 -> 17.13.0
+ MSTest.TestAdapter 2.1.2 -> 3.8.3
+ MSTest.TestFramework 2.1.2 -> 3.8.3
+ nunit 3.12.0 -> 4.3.2
+ NUnit3TestAdapter 3.17.0 -> 5.0.0
+ RichardSzalay.MockHttp 6.0.0 -> 7.0.0
+ xunit.extensibility.core 2.4.2 -> 2.9.3
## [1.3.0] - 2024-12-30
+
### Changed
-- Descriptions of ClientCertificate suboptions includes clearer information about usage in terms of different types of ClientCertificate.
+
+- Descriptions of ClientCertificate suboptions includes clearer information about usage in terms of different types of
+ ClientCertificate.
## [1.2.0] - 2024-08-19
+
### Changed
-- Removed handling where only PATCH, PUT, POST and DELETE requests were allowed to have the Content-Type header and content, due to HttpClient failing if e.g., a GET request had content. HttpClient has since been updated to tolerate such requests.
+
+- Removed handling where only PATCH, PUT, POST and DELETE requests were allowed to have the Content-Type header and
+ content, due to HttpClient failing if e.g., a GET request had content. HttpClient has since been updated to tolerate
+ such requests.
## [1.1.2] - 2024-01-16
+
### Fixed
-- Fixed misleading documentation.
+
+- Fixed misleading documentation.
## [1.1.1] - 2023-06-09
+
### Fixed
-- Fixed issue with terminating the Task by adding cancellationToken to the method ReadAsStringAsync when JToken as ReturnFormat.
+
+- Fixed issue with terminating the Task by adding cancellationToken to the method ReadAsStringAsync when JToken as
+ ReturnFormat.
## [1.1.0] - 2023-05-08
+
### Changed
-- [Breaking] Changed ResultMethod to ReturnFormat which describes the parameter better.
+
+- [Breaking] Changed ResultMethod to ReturnFormat which describes the parameter better.
- Fixed documentation link in the main method.
## [1.0.0] - 2023-01-24
+
### Added
+
- Initial implementation of Frends.HTTP.Request.
diff --git a/Frends.HTTP.Request/Frends.HTTP.Request.Tests/Frends.HTTP.Request.Tests.csproj b/Frends.HTTP.Request/Frends.HTTP.Request.Tests/Frends.HTTP.Request.Tests.csproj
index b882c61..73c1795 100644
--- a/Frends.HTTP.Request/Frends.HTTP.Request.Tests/Frends.HTTP.Request.Tests.csproj
+++ b/Frends.HTTP.Request/Frends.HTTP.Request.Tests/Frends.HTTP.Request.Tests.csproj
@@ -18,12 +18,10 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
\ No newline at end of file
+
diff --git a/Frends.HTTP.Request/Frends.HTTP.Request.Tests/MockHttpClientFactory.cs b/Frends.HTTP.Request/Frends.HTTP.Request.Tests/MockHttpClientFactory.cs
deleted file mode 100644
index 5d70505..0000000
--- a/Frends.HTTP.Request/Frends.HTTP.Request.Tests/MockHttpClientFactory.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.Net.Http;
-using Frends.HTTP.Request.Definitions;
-using RichardSzalay.MockHttp;
-
-namespace Frends.HTTP.Request.Tests;
-
-public class MockHttpClientFactory : IHttpClientFactory
-{
- private readonly MockHttpMessageHandler _mockHttpMessageHandler;
-
- public MockHttpClientFactory(MockHttpMessageHandler mockHttpMessageHandler)
- {
- _mockHttpMessageHandler = mockHttpMessageHandler;
- }
- public HttpClient CreateClient(Options options)
- {
- return _mockHttpMessageHandler.ToHttpClient();
- }
-}
diff --git a/Frends.HTTP.Request/Frends.HTTP.Request.Tests/UnitTests.cs b/Frends.HTTP.Request/Frends.HTTP.Request.Tests/UnitTests.cs
index 79512d9..1b2c78f 100644
--- a/Frends.HTTP.Request/Frends.HTTP.Request.Tests/UnitTests.cs
+++ b/Frends.HTTP.Request/Frends.HTTP.Request.Tests/UnitTests.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading;
@@ -7,10 +6,9 @@
using Frends.HTTP.Request.Definitions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Method = Frends.HTTP.Request.Definitions;
-using RichardSzalay.MockHttp;
using Assert = NUnit.Framework.Assert;
using System.Net;
-using System.Text;
+using System.Security.Cryptography.X509Certificates;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
@@ -21,15 +19,12 @@ namespace Frends.HTTP.Request.Tests;
[TestClass]
public class UnitTests
{
- private const string BasePath = "http://localhost:9191";
- private MockHttpMessageHandler _mockHttpMessageHandler;
+ private const string BasePath = "https://httpbin.org";
[TestInitialize]
public void TestInitialize()
{
- _mockHttpMessageHandler = new MockHttpMessageHandler();
HTTP.ClearClientCache();
- HTTP.ClientFactory = new MockHttpClientFactory(_mockHttpMessageHandler);
}
private static Input GetInputParams(Method.Method method = Method.Method.GET, string url = BasePath,
@@ -48,58 +43,16 @@ private static Input GetInputParams(Method.Method method = Method.Method.GET, st
[TestMethod]
public async Task RequestTestGetWithParameters()
{
- const string expectedReturn = @"'FooBar'";
- var dict = new Dictionary()
- {
- {
- "foo", "bar"
- },
- {
- "bar", "foo"
- }
- };
-
- _mockHttpMessageHandler.When($"{BasePath}/endpoint").WithQueryString(dict)
- .Respond("application/json", expectedReturn);
-
- var input = GetInputParams(url: "http://localhost:9191/endpoint?foo=bar&bar=foo");
+ var expected = "\"args\": {\n \"id\": \"2\", \n \"userId\": \"1\"\n }";
+ var input = GetInputParams(url: $"{BasePath}/anything?id=2&userId=1");
var options = new Options
{
- ConnectionTimeoutSeconds = 60
+ ConnectionTimeoutSeconds = 60,
};
- var result = (dynamic)await HTTP.Request(input, options, CancellationToken.None);
-
- ClassicAssert.IsTrue(result.Body.Contains("FooBar"));
- }
-
- [TestMethod]
- public async Task RequestTestGetWithContent()
- {
- const string expectedReturn = "OK";
- _mockHttpMessageHandler.When($"{BasePath}/endpoint").WithHeaders("Content-Type", "text/plain")
- .Respond("text/plain", expectedReturn);
-
- var contentType = new Header
- {
- Name = "Content-Type",
- Value = "text/plain"
- };
- var input = GetInputParams(
- url: "http://localhost:9191/endpoint",
- method: Method.Method.GET,
- headers: new Header[1]
- {
- contentType
- }
- );
- var options = new Options
- {
- ConnectionTimeoutSeconds = 60
- };
+ var result = await HTTP.Request(input, options, CancellationToken.None);
- var result = (dynamic)await HTTP.Request(input, options, CancellationToken.None);
- NUnit.Framework.Legacy.StringAssert.Contains(result.Body, "OK");
+ ClassicAssert.IsTrue(result.Body.Contains(expected));
}
[TestMethod]
@@ -125,17 +78,12 @@ public void RequestShouldThrowExceptionIfUrlEmpty()
}
[TestMethod]
- public void RequestShuldThrowExceptionIfOptionIsSet()
+ public void RequestShouldThrowExceptionIfOptionIsSet()
{
- const string expectedReturn = @"'FooBar'";
-
- _mockHttpMessageHandler.When($"{BasePath}/endpoint")
- .Respond(HttpStatusCode.InternalServerError, "application/json", expectedReturn);
-
var input = new Input
{
Method = Method.Method.GET,
- Url = "http://localhost:9191/endpoint",
+ Url = $"{BasePath}/invalid",
Headers = new Header[0],
Message = ""
};
@@ -148,22 +96,17 @@ public void RequestShuldThrowExceptionIfOptionIsSet()
var ex = Assert.ThrowsAsync(async () =>
await HTTP.Request(input, options, CancellationToken.None));
- ClassicAssert.IsTrue(ex.Message.Contains("FooBar"));
+ ClassicAssert.IsTrue(
+ ex.Message.Contains("Request to 'https://httpbin.org/invalid' failed with status code 404"));
}
[TestMethod]
public async Task RequestShouldNotThrowIfOptionIsNotSet()
{
- const string expectedReturn = @"'FooBar'";
-
- _mockHttpMessageHandler.When($"{BasePath}/endpoint")
- .Respond(HttpStatusCode.InternalServerError, "application/json", expectedReturn);
-
-
var input = new Input
{
Method = Method.Method.GET,
- Url = "http://localhost:9191/endpoint",
+ Url = $"{BasePath}/invalid",
Headers = new Header[0],
Message = ""
};
@@ -173,54 +116,41 @@ public async Task RequestShouldNotThrowIfOptionIsNotSet()
ThrowExceptionOnErrorResponse = false
};
- var result = (dynamic)await HTTP.Request(input, options, CancellationToken.None);
+ var result = await HTTP.Request(input, options, CancellationToken.None);
- ClassicAssert.IsTrue(result.Body.Contains("FooBar"));
+ ClassicAssert.IsTrue(
+ result.Body.Contains("404 Not Found"));
}
[TestMethod]
public async Task RequestShouldAddBasicAuthHeaders()
{
- const string expectedReturn = @"'FooBar'";
-
var input = new Input
{
Method = Method.Method.GET,
- Url = " http://localhost:9191/endpoint",
+ Url = $"{BasePath}/anything",
Headers = new Header[0],
Message = ""
};
-
var options = new Options
{
ConnectionTimeoutSeconds = 60,
ThrowExceptionOnErrorResponse = true,
Authentication = Authentication.Basic,
- Username = Guid.NewGuid().ToString(),
- Password = Guid.NewGuid().ToString()
+ Username = "username",
+ Password = "password",
};
-
- var sentAuthValue =
- "Basic " + Convert.ToBase64String(Encoding.ASCII.GetBytes($"{options.Username}:{options.Password}"));
-
- _mockHttpMessageHandler.Expect($"{BasePath}/endpoint").WithHeaders("Authorization", sentAuthValue)
- .Respond("application/json", expectedReturn);
-
- var result = (dynamic)await HTTP.Request(input, options, CancellationToken.None);
-
- _mockHttpMessageHandler.VerifyNoOutstandingExpectation();
- ClassicAssert.IsTrue(result.Body.Contains("FooBar"));
+ var result = await HTTP.Request(input, options, CancellationToken.None);
+ ClassicAssert.IsTrue(result.Body.Contains("\"Authorization\": \"Basic dXNlcm5hbWU6cGFzc3dvcmQ=\""));
}
[TestMethod]
public async Task RequestShouldAddOAuthBearerHeader()
{
- const string expectedReturn = @"'FooBar'";
-
var input = new Input
{
Method = Method.Method.GET,
- Url = "http://localhost:9191/endpoint",
+ Url = $"{BasePath}/anything",
Headers = new Header[0],
Message = ""
};
@@ -231,23 +161,18 @@ public async Task RequestShouldAddOAuthBearerHeader()
Token = "fooToken"
};
- _mockHttpMessageHandler.Expect($"{BasePath}/endpoint").WithHeaders("Authorization", "Bearer fooToken")
- .Respond("application/json", expectedReturn);
var result = await HTTP.Request(input, options, CancellationToken.None);
- _mockHttpMessageHandler.VerifyNoOutstandingExpectation();
- ClassicAssert.IsTrue(result.Body.Contains("FooBar"));
+ ClassicAssert.IsTrue(result.Body.Contains("\"Authorization\": \"Bearer fooToken\""));
}
[TestMethod]
public async Task AuthorizationHeaderShouldOverrideOption()
{
- const string expectedReturn = @"'FooBar'";
-
var input = new Input
{
Method = Method.Method.GET,
- Url = "http://localhost:9191/endpoint",
+ Url = $"{BasePath}/anything",
Headers = new[]
{
new Header()
@@ -265,12 +190,9 @@ public async Task AuthorizationHeaderShouldOverrideOption()
Token = "barToken"
};
- _mockHttpMessageHandler.Expect($"{BasePath}/endpoint").WithHeaders("Authorization", "Basic fooToken")
- .Respond("application/json", expectedReturn);
var result = await HTTP.Request(input, options, CancellationToken.None);
- _mockHttpMessageHandler.VerifyNoOutstandingExpectation();
- ClassicAssert.IsTrue(result.Body.Contains("FooBar"));
+ ClassicAssert.IsTrue(result.Body.Contains("\"Authorization\": \"Basic fooToken\""));
}
[TestMethod]
@@ -280,7 +202,7 @@ public void RequestShouldAddClientCertificate()
var input = new Input
{
Method = Method.Method.GET,
- Url = "http://localhost:9191/endpoint",
+ Url = BasePath,
Headers = new Header[0],
Message = "",
ResultMethod = ReturnFormat.JToken
@@ -293,8 +215,6 @@ public void RequestShouldAddClientCertificate()
CertificateThumbprint = thumbprint
};
- HTTP.ClientFactory = new HttpClientFactory();
-
var ex = Assert.ThrowsAsync(async () =>
await HTTP.Request(input, options, CancellationToken.None));
@@ -304,17 +224,10 @@ public void RequestShouldAddClientCertificate()
[TestMethod]
public async Task RestRequestBodyReturnShouldBeOfTypeJToken()
{
- dynamic dyn = new
- {
- Foo = "Bar"
- };
- string output = JsonConvert.SerializeObject(dyn);
-
-
var input = new Input
{
Method = Method.Method.GET,
- Url = "http://localhost:9191/endpoint",
+ Url = $"{BasePath}/anything",
Headers = new Header[0],
Message = "",
ResultMethod = ReturnFormat.JToken
@@ -326,12 +239,9 @@ public async Task RestRequestBodyReturnShouldBeOfTypeJToken()
Token = "fooToken"
};
- _mockHttpMessageHandler.When(input.Url)
- .Respond("application/json", output);
-
var result = await HTTP.Request(input, options, CancellationToken.None);
var resultBody = (JToken)result.Body;
- ClassicAssert.AreEqual(new JValue("Bar"), resultBody["Foo"]);
+ ClassicAssert.AreEqual(new JValue("GET"), resultBody["method"]);
}
[TestMethod]
@@ -340,7 +250,7 @@ public async Task RestRequestShouldNotThrowIfReturnIsEmpty()
var input = new Input
{
Method = Method.Method.GET,
- Url = "http://localhost:9191/endpoint",
+ Url = $"{BasePath}/status/200",
Headers = new Header[0],
Message = "",
ResultMethod = ReturnFormat.JToken
@@ -352,9 +262,6 @@ public async Task RestRequestShouldNotThrowIfReturnIsEmpty()
Token = "fooToken"
};
- _mockHttpMessageHandler.When(input.Url)
- .Respond("application/json", String.Empty);
-
var result = await HTTP.Request(input, options, CancellationToken.None);
ClassicAssert.AreEqual(new JValue(""), result.Body);
@@ -366,7 +273,7 @@ public void RestRequestShouldThrowIfReturnIsNotValidJson()
var input = new Input
{
Method = Method.Method.GET,
- Url = "http://localhost:9191/endpoint",
+ Url = $"{BasePath}/xml",
Headers = new Header[0],
Message = "",
ResultMethod = ReturnFormat.JToken
@@ -378,23 +285,23 @@ public void RestRequestShouldThrowIfReturnIsNotValidJson()
Token = "fooToken"
};
- _mockHttpMessageHandler.When(input.Url)
- .Respond("application/json", "failbar");
+ // _mockHttpMessageHandler.When(input.Url)
+ // .Respond("application/json", "failbar");
var ex = Assert.ThrowsAsync(async () =>
await HTTP.Request(input, options, CancellationToken.None));
- ClassicAssert.AreEqual("Unable to read response message as json: failbar", ex.Message);
+ Assert.That(ex.Message.Contains("Unable to read response message as json"));
}
[TestMethod]
public async Task HttpRequestBodyReturnShouldBeOfTypeString()
{
- const string expectedReturn = "BAR";
+ const string expectedReturn = "";
var input = new Input
{
Method = Method.Method.GET,
- Url = "http://localhost:9191/endpoint",
+ Url = $"{BasePath}/xml",
Headers = new Header[0],
Message = "",
ResultMethod = ReturnFormat.String
@@ -404,11 +311,9 @@ public async Task HttpRequestBodyReturnShouldBeOfTypeString()
ConnectionTimeoutSeconds = 60
};
- _mockHttpMessageHandler.When(input.Url)
- .Respond("text/plain", expectedReturn);
var result = await HTTP.Request(input, options, CancellationToken.None);
- ClassicAssert.AreEqual(expectedReturn, result.Body);
+ Assert.That(result.Body.Contains(expectedReturn), result.Body);
}
[TestMethod]
@@ -419,7 +324,7 @@ public async Task PatchShouldComeThrough()
var input = new Input
{
Method = Method.Method.PATCH,
- Url = "http://localhost:9191/endpoint",
+ Url = $"{BasePath}/anything",
Headers = new Header[]
{
},
@@ -431,13 +336,9 @@ public async Task PatchShouldComeThrough()
ConnectionTimeoutSeconds = 60
};
- _mockHttpMessageHandler.Expect(new HttpMethod("PATCH"), input.Url).WithContent(message)
- .Respond("text/plain", "foo åäö");
-
var result = await HTTP.Request(input, options, CancellationToken.None);
- _mockHttpMessageHandler.VerifyNoOutstandingExpectation();
- ClassicAssert.IsTrue(result.Body.Contains("foo åäö"));
+ ClassicAssert.IsTrue(result.Body.Contains("\"data\": \"\\u00e5\\u00e4\\u00f6\""), result.Body);
}
[TestMethod]
@@ -455,7 +356,7 @@ public async Task RequestShouldSetEncodingWithContentTypeCharsetIgnoringCase()
var input = new Input
{
Method = Method.Method.POST,
- Url = "http://localhost:9191/endpoint",
+ Url = $"{BasePath}/anything",
Headers = new Header[1]
{
contentType
@@ -468,27 +369,16 @@ public async Task RequestShouldSetEncodingWithContentTypeCharsetIgnoringCase()
ConnectionTimeoutSeconds = 60
};
- _mockHttpMessageHandler.Expect(HttpMethod.Post, input.Url).WithHeaders("cONTENT-tYpE", expectedContentType)
- .WithContent(requestMessage)
- .Respond("text/plain", "foo åäö");
-
var result = await HTTP.Request(input, options, CancellationToken.None);
- _mockHttpMessageHandler.VerifyNoOutstandingExpectation();
- ClassicAssert.IsTrue(result.Body.Contains("foo åäö"));
+ ClassicAssert.IsTrue(result.Body.Contains("\"Content-Type\": \"text/plain; charset=iso-8859-1\""));
}
[TestMethod]
public async Task RequestTest_GetMethod_ShouldSendEmptyContent()
{
- const string expectedReturn = "OK";
-
- _mockHttpMessageHandler.When($"{BasePath}/endpoint")
- .WithContent(string.Empty)
- .Respond("text/plain", expectedReturn);
-
var input = GetInputParams(
- url: "http://localhost:9191/endpoint",
+ url: $"{BasePath}/anything",
method: Method.Method.GET,
message: "This should not be sent"
);
@@ -499,7 +389,7 @@ public async Task RequestTest_GetMethod_ShouldSendEmptyContent()
};
var result = await HTTP.Request(input, options, CancellationToken.None);
- ClassicAssert.AreEqual(expectedReturn, result.Body);
+ Assert.That(result.Body.Contains("\"data\": \"\""), result.Body);
}
[TestCase(CertificateStoreLocation.CurrentUser, "current user")]
@@ -507,6 +397,7 @@ public async Task RequestTest_GetMethod_ShouldSendEmptyContent()
public void CorrectStoreSearched(CertificateStoreLocation storeLocation, string storeLocationText)
{
var handler = new HttpClientHandler();
+ X509Certificate2[] certificates = Array.Empty();
var options = new Options
{
Authentication = Authentication.ClientCertificate,
@@ -514,7 +405,8 @@ public void CorrectStoreSearched(CertificateStoreLocation storeLocation, string
CertificateStoreLocation = storeLocation,
CertificateThumbprint = "InvalidThumbprint",
};
- var ex = Assert.Throws(() => handler.SetHandlerSettingsBasedOnOptions(options));
+ var ex = Assert.Throws(() =>
+ handler.SetHandlerSettingsBasedOnOptions(options, ref certificates));
Assert.That(ex, Is.Not.Null);
Assert.That(ex.Message.Contains(
$"Certificate with thumbprint: 'INVALIDTHUMBPRINT' not found in {storeLocationText} cert store."));
diff --git a/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/IHttpClientFactory.cs b/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/IHttpClientFactory.cs
deleted file mode 100644
index e18d2f7..0000000
--- a/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/IHttpClientFactory.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using System.Net.Http;
-
-namespace Frends.HTTP.Request.Definitions;
-
-///
-/// Http Client Factory Interface
-///
-public interface IHttpClientFactory
-{
- ///
- /// Create client
- ///
- HttpClient CreateClient(Options options);
-}
diff --git a/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Options.cs b/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Options.cs
index 12bfe0a..0da8008 100644
--- a/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Options.cs
+++ b/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Options.cs
@@ -135,4 +135,11 @@ public class Options
/// true
[DefaultValue(true)]
public bool AutomaticCookieHandling { get; set; } = true;
+
+ ///
+ /// Define to use a caching mechanism for HttpClient.
+ ///
+ /// true
+ [DefaultValue(true)]
+ public bool CacheHttpClient { get; set; } = true;
}
diff --git a/Frends.HTTP.Request/Frends.HTTP.Request/Extensions.cs b/Frends.HTTP.Request/Frends.HTTP.Request/Extensions.cs
index eae9dc8..488db48 100644
--- a/Frends.HTTP.Request/Frends.HTTP.Request/Extensions.cs
+++ b/Frends.HTTP.Request/Frends.HTTP.Request/Extensions.cs
@@ -13,7 +13,7 @@ namespace Frends.HTTP.Request;
[ExcludeFromCodeCoverage]
internal static class Extensions
{
- internal static void SetHandlerSettingsBasedOnOptions(this HttpClientHandler handler, Options options)
+ internal static void SetHandlerSettingsBasedOnOptions(this HttpClientHandler handler, Options options, ref X509Certificate2[] certificates)
{
switch (options.Authentication)
{
@@ -35,7 +35,7 @@ internal static void SetHandlerSettingsBasedOnOptions(this HttpClientHandler han
break;
case Authentication.ClientCertificate:
- handler.ClientCertificates.AddRange(GetCertificates(options));
+ handler.ClientCertificates.AddRange(GetCertificates(options, ref certificates));
break;
}
@@ -57,10 +57,8 @@ internal static void SetDefaultRequestHeadersBasedOnOptions(this HttpClient http
httpClient.Timeout = TimeSpan.FromSeconds(Convert.ToDouble(options.ConnectionTimeoutSeconds));
}
- private static X509Certificate[] GetCertificates(Options options)
+ private static X509Certificate[] GetCertificates(Options options, ref X509Certificate2[] certificates)
{
- X509Certificate2[] certificates;
-
switch (options.ClientCertificateSource)
{
case CertificateSource.CertificateStore:
@@ -125,40 +123,39 @@ private static X509Certificate2[] GetCertificatesFromStore(string thumbprint,
? "current user"
: "local machine";
- using (var store = new X509Store(StoreName.My, location))
- {
- store.Open(OpenFlags.ReadOnly);
- var signingCert = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false);
+ using var store = new X509Store(StoreName.My, location);
- if (signingCert.Count == 0)
- {
- throw new FileNotFoundException(
- $"Certificate with thumbprint: '{thumbprint}' not found in {locationText} cert store.");
- }
+ store.Open(OpenFlags.ReadOnly);
+ var signingCert = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false);
+
+ if (signingCert.Count == 0)
+ {
+ throw new FileNotFoundException(
+ $"Certificate with thumbprint: '{thumbprint}' not found in {locationText} cert store.");
+ }
- var certificate = signingCert[0];
+ var certificate = signingCert[0];
- if (!loadEntireChain)
+ if (!loadEntireChain)
+ {
+ return new[]
{
- return new[]
- {
- certificate
- };
- }
-
- using var chain = new X509Chain();
- chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
- chain.Build(certificate);
-
- // include the whole chain
- var certificates = chain
- .ChainElements.Cast()
- .Select(c => c.Certificate)
- .OrderByDescending(c => c.HasPrivateKey)
- .ToArray();
-
- return certificates;
+ certificate
+ };
}
+
+ using var chain = new X509Chain();
+ chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
+ chain.Build(certificate);
+
+ // include the whole chain
+ var certificates = chain
+ .ChainElements
+ .Select(c => c.Certificate)
+ .OrderByDescending(c => c.HasPrivateKey)
+ .ToArray();
+
+ return certificates;
}
}
diff --git a/Frends.HTTP.Request/Frends.HTTP.Request/Frends.HTTP.Request.csproj b/Frends.HTTP.Request/Frends.HTTP.Request/Frends.HTTP.Request.csproj
index fe52021..edda118 100644
--- a/Frends.HTTP.Request/Frends.HTTP.Request/Frends.HTTP.Request.csproj
+++ b/Frends.HTTP.Request/Frends.HTTP.Request/Frends.HTTP.Request.csproj
@@ -2,7 +2,7 @@
net6.0
- 1.7.0
+ 1.8.0
Frends
Frends
Frends
diff --git a/Frends.HTTP.Request/Frends.HTTP.Request/HttpClientFactory.cs b/Frends.HTTP.Request/Frends.HTTP.Request/HttpClientFactory.cs
deleted file mode 100644
index 3187fd8..0000000
--- a/Frends.HTTP.Request/Frends.HTTP.Request/HttpClientFactory.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using System.Net.Http;
-using Frends.HTTP.Request.Definitions;
-
-namespace Frends.HTTP.Request;
-
-internal class HttpClientFactory : IHttpClientFactory
-{
- public HttpClient CreateClient(Options options)
- {
- var handler = new HttpClientHandler();
- handler.SetHandlerSettingsBasedOnOptions(options);
- return new HttpClient(handler);
- }
-}
diff --git a/Frends.HTTP.Request/Frends.HTTP.Request/Request.cs b/Frends.HTTP.Request/Frends.HTTP.Request/Request.cs
index 9462ea2..792f5f7 100644
--- a/Frends.HTTP.Request/Frends.HTTP.Request/Request.cs
+++ b/Frends.HTTP.Request/Frends.HTTP.Request/Request.cs
@@ -1,247 +1,311 @@
-using Frends.HTTP.Request.Definitions;
-using System.Collections.Generic;
-using System;
-using System.ComponentModel;
-using System.Linq;
-using System.Net;
-using System.Net.Http.Headers;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using Newtonsoft.Json.Linq;
-using Newtonsoft.Json;
-using System.Diagnostics;
-using System.Net.Http;
-using System.Runtime.Caching;
-using System.Runtime.CompilerServices;
-using System.Diagnostics.CodeAnalysis;
-
-[assembly: InternalsVisibleTo("Frends.HTTP.Request.Tests")]
-namespace Frends.HTTP.Request;
-
-///
-/// Task class.
-///
-public class HTTP
-{
- internal static IHttpClientFactory ClientFactory = new HttpClientFactory();
- internal static readonly ObjectCache ClientCache = MemoryCache.Default;
- private static readonly CacheItemPolicy _cachePolicy = new CacheItemPolicy() { SlidingExpiration = TimeSpan.FromHours(1) };
-
- internal static void ClearClientCache()
- {
- var cacheKeys = ClientCache.Select(kvp => kvp.Key).ToList();
- foreach (var cacheKey in cacheKeys)
- {
- ClientCache.Remove(cacheKey);
- }
- }
-
- ///
- /// Frends Task for executing HTTP requests with String or JSON payload.
- /// [Documentation](https://tasks.frends.com/tasks/frends-tasks/Frends.HTTP.Request)
- ///
- ///
- ///
- ///
- /// Object { dynamic Body, Dictionary(string, string) Headers, int StatusCode }
- public static async Task Request(
- [PropertyTab] Input input,
- [PropertyTab] Options options,
- CancellationToken cancellationToken
- )
- {
- if (string.IsNullOrEmpty(input.Url)) throw new ArgumentNullException("Url can not be empty.");
-
- var httpClient = GetHttpClientForOptions(options);
- var headers = GetHeaderDictionary(input.Headers, options);
-
- using var content = GetContent(input, headers);
- using var responseMessage = await GetHttpRequestResponseAsync(
- httpClient,
- input.Method.ToString(),
- input.Url,
- content,
- headers,
- options,
- cancellationToken)
- .ConfigureAwait(false);
-
- dynamic response;
-
- switch (input.ResultMethod)
- {
- case ReturnFormat.String:
- var hbody = responseMessage.Content != null ? await responseMessage.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false) : null;
- var hstatusCode = (int)responseMessage.StatusCode;
- var hheaders = GetResponseHeaderDictionary(responseMessage.Headers, responseMessage.Content?.Headers);
- response = new Result(hbody, hheaders, hstatusCode);
- break;
- case ReturnFormat.JToken:
- var rbody = TryParseRequestStringResultAsJToken(await responseMessage.Content.ReadAsStringAsync(cancellationToken)
- .ConfigureAwait(false));
- var rstatusCode = (int)responseMessage.StatusCode;
- var rheaders = GetResponseHeaderDictionary(responseMessage.Headers, responseMessage.Content.Headers);
- response = new Result(rbody, rheaders, rstatusCode);
- break;
- default: throw new InvalidOperationException();
- }
-
- if (!responseMessage.IsSuccessStatusCode && options.ThrowExceptionOnErrorResponse)
- {
- throw new WebException(
- $"Request to '{input.Url}' failed with status code {(int)responseMessage.StatusCode}. Response body: {response.Body}");
- }
-
- return response;
- }
-
- // Combine response- and responsecontent header to one dictionary
- private static Dictionary GetResponseHeaderDictionary(HttpResponseHeaders responseMessageHeaders, HttpContentHeaders contentHeaders)
- {
- var responseHeaders = responseMessageHeaders.ToDictionary(h => h.Key, h => string.Join(";", h.Value));
- var allHeaders = contentHeaders?.ToDictionary(h => h.Key, h => string.Join(";", h.Value)) ?? new Dictionary();
- responseHeaders.ToList().ForEach(x => allHeaders[x.Key] = x.Value);
- return allHeaders;
- }
-
- private static IDictionary GetHeaderDictionary(Header[] headers, Options options)
- {
- if (!headers.Any(header => header.Name.ToLower().Equals("authorization")))
- {
-
- var authHeader = new Header { Name = "Authorization" };
- switch (options.Authentication)
- {
- case Authentication.Basic:
- authHeader.Value = $"Basic {Convert.ToBase64String(Encoding.ASCII.GetBytes($"{options.Username}:{options.Password}"))}";
- headers = headers.Concat(new[] { authHeader }).ToArray();
- break;
- case Authentication.OAuth:
- authHeader.Value = $"Bearer {options.Token}";
- headers = headers.Concat(new[] { authHeader }).ToArray();
- break;
- }
- }
-
- //Ignore case for headers and key comparison
- return headers.ToDictionary(key => key.Name, value => value.Value, StringComparer.InvariantCultureIgnoreCase);
- }
-
- private static HttpContent GetContent(Input input, IDictionary headers)
- {
- var methodsWithBody = new[] { Method.POST, Method.PUT, Method.PATCH, Method.DELETE };
-
- if (!methodsWithBody.Contains(input.Method))
- {
- return new StringContent(string.Empty);
- }
-
- if (headers.TryGetValue("content-type", out string contentTypeValue))
- {
- var contentTypeIsSetAndValid = MediaTypeWithQualityHeaderValue.TryParse(contentTypeValue, out var validContentType);
- if (contentTypeIsSetAndValid)
- return new StringContent(input.Message ?? string.Empty, Encoding.GetEncoding(validContentType.CharSet ?? Encoding.UTF8.WebName));
- }
-
- return new StringContent(input.Message ?? string.Empty);
- }
-
- private static object TryParseRequestStringResultAsJToken(string response)
- {
- try
- {
- return string.IsNullOrWhiteSpace(response) ? new JValue("") : JToken.Parse(response);
- }
- catch (JsonReaderException)
- {
- throw new JsonReaderException($"Unable to read response message as json: {response}");
- }
- }
-
- private static HttpClient GetHttpClientForOptions(Options options)
- {
- var cacheKey = GetHttpClientCacheKey(options);
-
- if (ClientCache.Get(cacheKey) is HttpClient httpClient)
- {
- return httpClient;
- }
-
- httpClient = ClientFactory.CreateClient(options);
- httpClient.SetDefaultRequestHeadersBasedOnOptions(options);
-
- ClientCache.Add(cacheKey, httpClient, _cachePolicy);
-
- return httpClient;
- }
-
- [ExcludeFromCodeCoverage]
- private static string GetHttpClientCacheKey(Options options)
- {
- // Includes everything except for options.Token, which is used on request level, not http client level
- return $"{options.Authentication}:{options.Username}:{options.Password}:{options.ClientCertificateSource}"
- + $":{options.ClientCertificateFilePath}:{options.ClientCertificateInBase64}:{options.ClientCertificateKeyPhrase}"
- + $":{options.CertificateThumbprint}:{options.LoadEntireChainForCertificate}:{options.ConnectionTimeoutSeconds}"
- + $":{options.FollowRedirects}:{options.AllowInvalidCertificate}:{options.AllowInvalidResponseContentTypeCharSet}"
- + $":{options.ThrowExceptionOnErrorResponse}:{options.AutomaticCookieHandling}";
- }
-
- private static async Task GetHttpRequestResponseAsync(
- HttpClient httpClient, string method, string url,
- HttpContent content, IDictionary headers,
- Options options, CancellationToken cancellationToken)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- using (var request = new HttpRequestMessage(new HttpMethod(method), new Uri(url))
- {
- Content = content
- })
- {
-
- //Clear default headers
- content.Headers.Clear();
- foreach (var header in headers)
- {
- var requestHeaderAddedSuccessfully = request.Headers.TryAddWithoutValidation(header.Key, header.Value);
- if (!requestHeaderAddedSuccessfully)
- {
- //Could not add to request headers try to add to content headers
- // this check is probably not needed anymore as the new HttpClient does not seem fail on malformed headers
- var contentHeaderAddedSuccessfully = content.Headers.TryAddWithoutValidation(header.Key, header.Value);
- if (!contentHeaderAddedSuccessfully)
- {
- Trace.TraceWarning($"Could not add header {header.Key}:{header.Value}");
- }
- }
- }
-
- HttpResponseMessage response;
- try
- {
- response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
- }
- catch (TaskCanceledException canceledException)
- {
- if (cancellationToken.IsCancellationRequested)
- {
- // Cancellation is from outside -> Just throw
- throw;
- }
-
- // Cancellation is from inside of the request, mostly likely a timeout
- throw new Exception("HttpRequest was canceled, most likely due to a timeout.", canceledException);
- }
-
-
- // this check is probably not needed anymore as the new HttpClient does not fail on invalid charsets
- if (options.AllowInvalidResponseContentTypeCharSet && response.Content.Headers?.ContentType != null)
- {
- response.Content.Headers.ContentType.CharSet = null;
- }
-
- return response;
- }
- }
-}
+using System.Collections.Generic;
+using System;
+using System.ComponentModel;
+using System.Linq;
+using System.Net;
+using System.Net.Http.Headers;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Newtonsoft.Json.Linq;
+using Newtonsoft.Json;
+using System.Diagnostics;
+using System.Net.Http;
+using System.Runtime.Caching;
+using System.Runtime.CompilerServices;
+using System.Diagnostics.CodeAnalysis;
+using System.Security.Cryptography.X509Certificates;
+using Frends.HTTP.Request.Definitions;
+
+[assembly: InternalsVisibleTo("Frends.HTTP.Request.Tests")]
+
+namespace Frends.HTTP.Request;
+
+///
+/// Task class.
+///
+public static class HTTP
+{
+ private static readonly ObjectCache ClientCache = MemoryCache.Default;
+
+ private static readonly CacheItemPolicy CachePolicy = new()
+ {
+ SlidingExpiration = TimeSpan.FromHours(1),
+ };
+
+ private static HttpContent httpContent;
+ private static HttpClient httpClient;
+ private static HttpClientHandler httpClientHandler;
+ private static HttpRequestMessage httpRequestMessage;
+ private static HttpResponseMessage httpResponseMessage;
+ private static X509Certificate2[] certificates = Array.Empty();
+
+
+ internal static void ClearClientCache()
+ {
+ var cacheKeys = ClientCache.Select(kvp => kvp.Key).ToList();
+
+ foreach (var cacheKey in cacheKeys)
+ {
+ ClientCache.Remove(cacheKey);
+ }
+ }
+
+ ///
+ /// Frends Task for executing HTTP requests with String or JSON payload.
+ /// [Documentation](https://tasks.frends.com/tasks/frends-tasks/Frends.HTTP.Request)
+ ///
+ ///
+ ///
+ ///
+ /// Object { dynamic Body, Dictionary(string, string) Headers, int StatusCode }
+ public static async Task Request(
+ [PropertyTab] Input input,
+ [PropertyTab] Options options,
+ CancellationToken cancellationToken
+ )
+ {
+ try
+ {
+ if (string.IsNullOrEmpty(input.Url)) throw new ArgumentNullException("Url can not be empty.");
+
+ httpClient = GetHttpClientForOptions(options);
+ var headers = GetHeaderDictionary(input.Headers, options);
+
+ httpContent = GetContent(input, headers);
+ using var responseMessage = await GetHttpRequestResponseAsync(
+ httpClient,
+ input.Method.ToString(),
+ input.Url,
+ httpContent,
+ headers,
+ options,
+ cancellationToken)
+ .ConfigureAwait(false);
+
+ Result response;
+
+ switch (input.ResultMethod)
+ {
+ case ReturnFormat.String:
+ var hbody = responseMessage.Content != null
+ ? await responseMessage.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false)
+ : null;
+ var hstatusCode = (int)responseMessage.StatusCode;
+ var hheaders =
+ GetResponseHeaderDictionary(responseMessage.Headers, responseMessage.Content?.Headers);
+ response = new Result(hbody, hheaders, hstatusCode);
+
+ break;
+ case ReturnFormat.JToken:
+ var rbody = TryParseRequestStringResultAsJToken(await responseMessage.Content
+ .ReadAsStringAsync(cancellationToken)
+ .ConfigureAwait(false));
+ var rstatusCode = (int)responseMessage.StatusCode;
+ var rheaders =
+ GetResponseHeaderDictionary(responseMessage.Headers, responseMessage.Content.Headers);
+ response = new Result(rbody, rheaders, rstatusCode);
+
+ break;
+ default: throw new InvalidOperationException();
+ }
+
+ if (!responseMessage.IsSuccessStatusCode && options.ThrowExceptionOnErrorResponse)
+ {
+ throw new WebException(
+ $"Request to '{input.Url}' failed with status code {(int)responseMessage.StatusCode}. Response body: {response.Body}");
+ }
+
+ return response;
+ }
+ finally
+ {
+ httpResponseMessage?.Dispose();
+ httpRequestMessage?.Dispose();
+ httpContent?.Dispose();
+
+ if (!options.CacheHttpClient)
+ {
+ foreach (var cert in certificates) cert?.Dispose();
+ httpClient?.Dispose();
+ httpClientHandler?.Dispose();
+ }
+ }
+ }
+
+ // Combine response- and responsecontent header to one dictionary
+ private static Dictionary GetResponseHeaderDictionary(HttpResponseHeaders responseMessageHeaders,
+ HttpContentHeaders contentHeaders)
+ {
+ var responseHeaders = responseMessageHeaders.ToDictionary(h => h.Key, h => string.Join(";", h.Value));
+ var allHeaders = contentHeaders?.ToDictionary(h => h.Key, h => string.Join(";", h.Value)) ??
+ new Dictionary();
+ responseHeaders.ToList().ForEach(x => allHeaders[x.Key] = x.Value);
+
+ return allHeaders;
+ }
+
+ private static IDictionary GetHeaderDictionary(Header[] headers, Options options)
+ {
+ if (!headers.Any(header => header.Name.ToLower().Equals("authorization")))
+ {
+ var authHeader = new Header
+ {
+ Name = "Authorization"
+ };
+
+ switch (options.Authentication)
+ {
+ case Authentication.Basic:
+ authHeader.Value =
+ $"Basic {Convert.ToBase64String(Encoding.ASCII.GetBytes($"{options.Username}:{options.Password}"))}";
+ headers = headers.Concat(new[]
+ {
+ authHeader
+ }).ToArray();
+
+ break;
+ case Authentication.OAuth:
+ authHeader.Value = $"Bearer {options.Token}";
+ headers = headers.Concat(new[]
+ {
+ authHeader
+ }).ToArray();
+
+ break;
+ }
+ }
+
+ //Ignore case for headers and key comparison
+ return headers.ToDictionary(key => key.Name, value => value.Value, StringComparer.InvariantCultureIgnoreCase);
+ }
+
+ private static HttpContent GetContent(Input input, IDictionary headers)
+ {
+ var methodsWithBody = new[]
+ {
+ Method.POST, Method.PUT, Method.PATCH, Method.DELETE
+ };
+
+ if (!methodsWithBody.Contains(input.Method))
+ {
+ return new StringContent(string.Empty);
+ }
+
+ if (headers.TryGetValue("content-type", out string contentTypeValue))
+ {
+ var contentTypeIsSetAndValid =
+ MediaTypeWithQualityHeaderValue.TryParse(contentTypeValue, out var validContentType);
+
+ if (contentTypeIsSetAndValid)
+ return new StringContent(input.Message ?? string.Empty,
+ Encoding.GetEncoding(validContentType.CharSet ?? Encoding.UTF8.WebName));
+ }
+
+ return new StringContent(input.Message ?? string.Empty);
+ }
+
+ private static object TryParseRequestStringResultAsJToken(string response)
+ {
+ try
+ {
+ return string.IsNullOrWhiteSpace(response) ? new JValue("") : JToken.Parse(response);
+ }
+ catch (JsonReaderException)
+ {
+ throw new JsonReaderException($"Unable to read response message as json: {response}");
+ }
+ }
+
+ private static HttpClient GetHttpClientForOptions(Options options)
+ {
+ string cacheKey = null;
+
+ if (options.CacheHttpClient)
+ {
+ cacheKey = GetHttpClientCacheKey(options);
+
+ if (ClientCache.Get(cacheKey) is HttpClient client)
+ {
+ return client;
+ }
+ }
+
+ httpClientHandler = new HttpClientHandler();
+ httpClientHandler.SetHandlerSettingsBasedOnOptions(options, ref certificates);
+ httpClient = new HttpClient(httpClientHandler);
+ httpClient.SetDefaultRequestHeadersBasedOnOptions(options);
+
+ if (cacheKey != null) ClientCache.Add(cacheKey, httpClient, CachePolicy);
+
+ return httpClient;
+ }
+
+ [ExcludeFromCodeCoverage]
+ private static string GetHttpClientCacheKey(Options options)
+ {
+ // Includes everything except for options.Token, which is used on request level, not http client level
+ return $"{options.Authentication}:{options.Username}:{options.Password}:{options.ClientCertificateSource}"
+ + $":{options.ClientCertificateFilePath}:{options.ClientCertificateInBase64}:{options.ClientCertificateKeyPhrase}"
+ + $":{options.CertificateThumbprint}:{options.LoadEntireChainForCertificate}:{options.ConnectionTimeoutSeconds}"
+ + $":{options.FollowRedirects}:{options.AllowInvalidCertificate}:{options.AllowInvalidResponseContentTypeCharSet}"
+ + $":{options.ThrowExceptionOnErrorResponse}:{options.AutomaticCookieHandling}";
+ }
+
+ private static async Task GetHttpRequestResponseAsync(
+ HttpClient client, string method, string url,
+ HttpContent content, IDictionary headers,
+ Options options, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ httpRequestMessage = new HttpRequestMessage(new HttpMethod(method), new Uri(url));
+ httpRequestMessage.Content = content;
+
+ //Clear default headers
+ content.Headers.Clear();
+
+ foreach (var header in headers)
+ {
+ var requestHeaderAddedSuccessfully =
+ httpRequestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value);
+
+ if (!requestHeaderAddedSuccessfully)
+ {
+ //Could not add to request headers try to add to content headers
+ // this check is probably not needed anymore as the new HttpClient does not seem fail on malformed headers
+ var contentHeaderAddedSuccessfully = content.Headers.TryAddWithoutValidation(header.Key, header.Value);
+
+ if (!contentHeaderAddedSuccessfully)
+ {
+ Trace.TraceWarning($"Could not add header {header.Key}:{header.Value}");
+ }
+ }
+ }
+
+ try
+ {
+ httpResponseMessage = await client.SendAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false);
+ }
+ catch (TaskCanceledException canceledException)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ // Cancellation is from outside -> Just throw
+ throw;
+ }
+
+ // Cancellation is from inside of the request, mostly likely a timeout
+ throw new Exception("HttpRequest was canceled, most likely due to a timeout.", canceledException);
+ }
+
+
+ // this check is probably not needed anymore as the new HttpClient does not fail on invalid charsets
+ if (options.AllowInvalidResponseContentTypeCharSet && httpResponseMessage.Content.Headers?.ContentType != null)
+ {
+ httpResponseMessage.Content.Headers.ContentType.CharSet = null;
+ }
+
+ return httpResponseMessage;
+ }
+}