diff --git a/DevBase.Net/Abstract/HttpKeyValueListBuilder.cs b/DevBase.Net/Abstract/HttpKeyValueListBuilder.cs
index 2476e9f..0b36c6f 100644
--- a/DevBase.Net/Abstract/HttpKeyValueListBuilder.cs
+++ b/DevBase.Net/Abstract/HttpKeyValueListBuilder.cs
@@ -78,8 +78,15 @@ protected void RemoveEntryValue(TKeyK value) =>
///
/// The key.
/// The value.
- protected TKeyV GetEntryValue(TKeyK key) =>
- this.Entries.FirstOrDefault(e => KeyEquals(e.Key, key)).Value;
+ protected TKeyV GetEntryValue(TKeyK key)
+ {
+ for (int i = 0; i < this.Entries.Count; i++)
+ {
+ if (KeyEquals(this.Entries[i].Key, key))
+ return this.Entries[i].Value;
+ }
+ return default!;
+ }
///
/// Gets the value at the specified index.
@@ -96,13 +103,13 @@ protected TKeyV GetEntryValue(int index) =>
/// The new value.
protected void SetEntryValue(TKeyK key, TKeyV value)
{
- int index = this.Entries
- .FindIndex(e => KeyEquals(e.Key, key));
-
- if (index >= 0)
+ for (int i = 0; i < this.Entries.Count; i++)
{
- TKeyK existingKey = this.Entries[index].Key;
- this.Entries[index] = KeyValuePair.Create(existingKey, value);
+ if (KeyEquals(this.Entries[i].Key, key))
+ {
+ this.Entries[i] = KeyValuePair.Create(this.Entries[i].Key, value);
+ return;
+ }
}
}
@@ -122,8 +129,15 @@ protected void SetEntryValue(int index, TKeyV value)
///
/// The key to check.
/// True if exists, false otherwise.
- protected bool AnyEntry(TKeyK key) =>
- this.Entries.Exists(e => KeyEquals(e.Key, key));
+ protected bool AnyEntry(TKeyK key)
+ {
+ for (int i = 0; i < this.Entries.Count; i++)
+ {
+ if (KeyEquals(this.Entries[i].Key, key))
+ return true;
+ }
+ return false;
+ }
private static bool KeyEquals(TKeyK? a, TKeyK? b)
{
diff --git a/DevBase.Net/Core/RequestHttp.cs b/DevBase.Net/Core/RequestHttp.cs
index 7b76621..1af60ca 100644
--- a/DevBase.Net/Core/RequestHttp.cs
+++ b/DevBase.Net/Core/RequestHttp.cs
@@ -28,7 +28,7 @@ public override Request Build()
if (this._requestBuilder.RequestHeaderBuilder != null)
{
- userHeaders = this._requestBuilder.RequestHeaderBuilder.GetEntries().ToList();
+ userHeaders = [..this._requestBuilder.RequestHeaderBuilder.GetEntries()];
userDefinedUserAgent = this._requestBuilder.RequestHeaderBuilder.GetPreBuildUserAgent();
}
@@ -101,9 +101,12 @@ public override async Task SendAsync(CancellationToken cancellationTok
int attemptNumber = 0;
System.Exception? lastException = null;
- foreach (Interfaces.IRequestInterceptor interceptor in this._requestInterceptors.OrderBy(i => i.Order))
+ if (this._requestInterceptors.Count > 0)
{
- await interceptor.OnRequestAsync(this, token);
+ foreach (Interfaces.IRequestInterceptor interceptor in this._requestInterceptors)
+ {
+ await interceptor.OnRequestAsync(this, token);
+ }
}
if (this._hostCheckConfig != null)
@@ -122,6 +125,8 @@ public override async Task SendAsync(CancellationToken cancellationTok
await Task.Delay(delay, token);
}
+ Uri requestUri = this.GetUri()!;
+
try
{
HttpClient client = this.GetOrCreateClient();
@@ -143,7 +148,7 @@ public override async Task SendAsync(CancellationToken cancellationTok
if (httpResponse.StatusCode == HttpStatusCode.TooManyRequests)
{
- RateLimitException rateLimitException = this.HandleRateLimitResponse(httpResponse);
+ RateLimitException rateLimitException = HandleRateLimitResponse(httpResponse, requestUri);
if (attemptNumber > this._retryPolicy.MaxRetries)
throw rateLimitException;
@@ -166,13 +171,16 @@ public override async Task SendAsync(CancellationToken cancellationTok
Response response = new Response(httpResponse, contentStream, metricsBuilder.Build())
{
- RequestUri = new Uri(this.Uri.ToString()),
+ RequestUri = requestUri,
FromCache = false
};
- foreach (Interfaces.IResponseInterceptor interceptor in this._responseInterceptors.OrderBy(i => i.Order))
+ if (this._responseInterceptors.Count > 0)
{
- await interceptor.OnResponseAsync(response, token);
+ foreach (Interfaces.IResponseInterceptor interceptor in this._responseInterceptors)
+ {
+ await interceptor.OnResponseAsync(response, token);
+ }
}
return response;
@@ -183,7 +191,7 @@ public override async Task SendAsync(CancellationToken cancellationTok
}
catch (OperationCanceledException)
{
- lastException = new RequestTimeoutException(this._timeout, new Uri(this.Uri.ToString()), attemptNumber);
+ lastException = new RequestTimeoutException(this._timeout, requestUri, attemptNumber);
if (attemptNumber > this._retryPolicy.MaxRetries)
throw lastException;
@@ -198,9 +206,7 @@ public override async Task SendAsync(CancellationToken cancellationTok
}
catch (HttpRequestException ex)
{
- string uri = this.Uri.ToString();
- string host = new Uri(uri).Host;
- lastException = new NetworkException(ex.Message, ex, host, attemptNumber);
+ lastException = new NetworkException(ex.Message, ex, requestUri.Host, attemptNumber);
if (attemptNumber > this._retryPolicy.MaxRetries)
throw lastException;
@@ -224,6 +230,9 @@ public HttpRequestMessage ToHttpRequestMessage()
{
foreach (KeyValuePair entry in this._requestBuilder.RequestHeaderBuilder.GetEntries())
{
+ if (entry.Key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase))
+ continue;
+
message.Headers.TryAddWithoutValidation(entry.Key, entry.Value);
}
}
@@ -233,7 +242,13 @@ public HttpRequestMessage ToHttpRequestMessage()
byte[] bodyArray = this.Body.ToArray();
message.Content = new ByteArrayContent(bodyArray);
- if (this._requestBuilder.RequestHeaderBuilder?.GetHeader("Content-Type") == null)
+ string? explicitContentType = this._requestBuilder.RequestHeaderBuilder?.GetHeader("Content-Type");
+
+ if (explicitContentType != null)
+ {
+ message.Content.Headers.TryAddWithoutValidation("Content-Type", explicitContentType);
+ }
+ else
{
if (this._formBuilder != null)
{
@@ -284,8 +299,6 @@ private string BuildClientKey()
sb.Append(this._validateCertificates);
sb.Append("|redirect:");
sb.Append(this._followRedirects);
- sb.Append("|httpver:");
- sb.Append(this._httpVersion);
return sb.ToStringAndRelease();
}
@@ -323,7 +336,7 @@ private SocketsHttpHandler CreateHandler()
private async Task CheckHostReachabilityAsync(CancellationToken cancellationToken)
{
- Uri uri = new Uri(this.Uri.ToString());
+ Uri uri = this.GetUri()!;
string host = uri.Host;
if (this._hostCheckConfig!.Method == EnumHostCheckMethod.Ping)
@@ -349,10 +362,9 @@ private async Task CheckHostReachabilityAsync(CancellationToken cancellationToke
private static bool IsProxyError(HttpRequestException ex)
{
- string message = ex.Message.ToLowerInvariant();
- return message.Contains("proxy") ||
- message.Contains("407") ||
- ex.StatusCode == HttpStatusCode.ProxyAuthenticationRequired;
+ return ex.StatusCode == HttpStatusCode.ProxyAuthenticationRequired ||
+ ex.Message.Contains("proxy", StringComparison.OrdinalIgnoreCase) ||
+ ex.Message.Contains("407", StringComparison.Ordinal);
}
private void ApplyScrapingBypassIfConfigured(List>? userHeaders, string? userDefinedUserAgent)
@@ -387,10 +399,8 @@ private void ApplyScrapingBypassIfConfigured(List>?
}
}
- private RateLimitException HandleRateLimitResponse(HttpResponseMessage response)
+ private static RateLimitException HandleRateLimitResponse(HttpResponseMessage response, Uri requestUri)
{
- Uri requestUri = new Uri(this.Uri.ToString());
-
if (response.Headers.TryGetValues("Retry-After", out IEnumerable? retryAfterValues))
{
string? retryAfter = retryAfterValues.FirstOrDefault();
diff --git a/DevBase.Net/DevBase.Net.csproj b/DevBase.Net/DevBase.Net.csproj
index afcf81b..c7460b0 100644
--- a/DevBase.Net/DevBase.Net.csproj
+++ b/DevBase.Net/DevBase.Net.csproj
@@ -15,7 +15,7 @@
https://github.com/AlexanderDotH/DevBase.git
https://github.com/AlexanderDotH/DevBase.git
git
- 1.4.3
+ 1.4.6
MIT
false
http;client;requests;proxy;socks5;jwt;authentication;fluent-api;async;retry;rate-limiting;json;html-parsing
diff --git a/DevBase.Test/DevBaseRequests/ContentTypeHeaderTest.cs b/DevBase.Test/DevBaseRequests/ContentTypeHeaderTest.cs
new file mode 100644
index 0000000..b13686d
--- /dev/null
+++ b/DevBase.Test/DevBaseRequests/ContentTypeHeaderTest.cs
@@ -0,0 +1,111 @@
+using System.Net.Http.Headers;
+using DevBase.Net.Core;
+
+namespace DevBase.Test.DevBaseRequests;
+
+[TestFixture]
+public class ContentTypeHeaderTest
+{
+ [Test]
+ public void ExplicitContentType_ShouldBeAppliedToContentHeaders()
+ {
+ Request request = new Request("https://example.com/api")
+ .WithHeader("Content-Type", "application/json")
+ .WithJsonBody("{\"test\": \"value\"}");
+
+ using HttpRequestMessage httpMessage = request.ToHttpRequestMessage();
+
+ Assert.That(httpMessage.Content, Is.Not.Null);
+ Assert.That(httpMessage.Content!.Headers.ContentType?.MediaType, Is.EqualTo("application/json"));
+ }
+
+ [Test]
+ public void ExplicitContentType_ShouldNotBeInRequestHeaders()
+ {
+ Request request = new Request("https://example.com/api")
+ .WithHeader("Content-Type", "application/json")
+ .WithJsonBody("{\"test\": \"value\"}");
+
+ using HttpRequestMessage httpMessage = request.ToHttpRequestMessage();
+
+ bool hasContentTypeInRequestHeaders = httpMessage.Headers
+ .Any(h => h.Key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase));
+ Assert.That(hasContentTypeInRequestHeaders, Is.False, "Content-Type should not be in request headers");
+ }
+
+ [Test]
+ public void CustomContentType_ShouldBePreserved()
+ {
+ const string customContentType = "application/vnd.api+json";
+
+ Request request = new Request("https://example.com/api")
+ .WithHeader("Content-Type", customContentType)
+ .WithJsonBody("{\"data\": {}}");
+
+ using HttpRequestMessage httpMessage = request.ToHttpRequestMessage();
+
+ Assert.That(httpMessage.Content, Is.Not.Null);
+ Assert.That(httpMessage.Content!.Headers.ContentType?.MediaType, Is.EqualTo(customContentType));
+ }
+
+ [Test]
+ public void NoExplicitContentType_ShouldDefaultToJsonForJsonBody()
+ {
+ Request request = new Request("https://example.com/api")
+ .WithJsonBody("{\"test\": \"value\"}");
+
+ using HttpRequestMessage httpMessage = request.ToHttpRequestMessage();
+
+ Assert.That(httpMessage.Content, Is.Not.Null);
+ Assert.That(httpMessage.Content!.Headers.ContentType?.MediaType, Is.EqualTo("application/json"));
+ }
+
+ [Test]
+ public void ContentTypeWithCharset_ShouldBePreserved()
+ {
+ const string contentTypeWithCharset = "application/json; charset=utf-8";
+
+ Request request = new Request("https://example.com/api")
+ .WithHeader("Content-Type", contentTypeWithCharset)
+ .WithJsonBody("{\"test\": \"value\"}");
+
+ using HttpRequestMessage httpMessage = request.ToHttpRequestMessage();
+
+ Assert.That(httpMessage.Content, Is.Not.Null);
+ string? fullContentType = httpMessage.Content!.Headers.ContentType?.ToString();
+ Assert.That(fullContentType, Is.EqualTo(contentTypeWithCharset));
+ }
+
+ [Test]
+ public void OtherHeaders_ShouldStillBeInRequestHeaders()
+ {
+ Request request = new Request("https://example.com/api")
+ .WithHeader("Content-Type", "application/json")
+ .WithHeader("X-Custom-Header", "custom-value")
+ .WithHeader("Authorization", "Bearer token123")
+ .WithJsonBody("{\"test\": \"value\"}");
+
+ using HttpRequestMessage httpMessage = request.ToHttpRequestMessage();
+
+ Assert.That(httpMessage.Headers.Contains("X-Custom-Header"), Is.True);
+ Assert.That(httpMessage.Headers.Contains("Authorization"), Is.True);
+ Assert.That(httpMessage.Headers.GetValues("X-Custom-Header").First(), Is.EqualTo("custom-value"));
+ }
+
+ [Test]
+ public void ContentTypeCaseInsensitive_ShouldBeHandledCorrectly()
+ {
+ Request request = new Request("https://example.com/api")
+ .WithHeader("content-type", "application/json")
+ .WithJsonBody("{\"test\": \"value\"}");
+
+ using HttpRequestMessage httpMessage = request.ToHttpRequestMessage();
+
+ bool hasContentTypeInRequestHeaders = httpMessage.Headers
+ .Any(h => h.Key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase));
+ Assert.That(hasContentTypeInRequestHeaders, Is.False, "Content-Type (any case) should not be in request headers");
+
+ Assert.That(httpMessage.Content, Is.Not.Null);
+ Assert.That(httpMessage.Content!.Headers.ContentType?.MediaType, Is.EqualTo("application/json"));
+ }
+}
diff --git a/DevBase.Test/DevBaseRequests/FileUploadTest.cs b/DevBase.Test/DevBaseRequests/FileUploadTest.cs
index 0d5fe34..f063b1a 100644
--- a/DevBase.Test/DevBaseRequests/FileUploadTest.cs
+++ b/DevBase.Test/DevBaseRequests/FileUploadTest.cs
@@ -283,9 +283,8 @@ public void Request_WithCustomContentType_DoesNotOverwrite()
request.Build();
var httpMessage = request.ToHttpRequestMessage();
- // Custom Content-Type is in request headers, not message headers
- // The Content-Type should not be overwritten by multipart detection
- Assert.That(httpMessage.Content!.Headers.ContentType, Is.Null);
+ // Custom Content-Type should be preserved in content headers, not overwritten by multipart detection
+ Assert.That(httpMessage.Content!.Headers.ContentType?.MediaType, Is.EqualTo("custom/type"));
}
#endregion