-
Notifications
You must be signed in to change notification settings - Fork 2
Fspes 138 #58
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fspes 138 #58
Changes from all commits
5db6c2b
c66593e
505be3e
dc8b8e8
c8d607a
b8bfb84
a7e9e45
f10d324
44334aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,11 @@ | ||
| # Changelog | ||
|
|
||
| ## [1.12.0] - 2026-06-12 | ||
|
|
||
| ### Fixed | ||
|
|
||
| - Fixed an issue related to concurrency problem with static fields. | ||
|
Comment on lines
+3
to
+7
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Document the .NET 8 target bump as a breaking change. This release also changes 🧰 Tools🪛 LanguageTool[style] ~7-~7: Consider using a different verb for a more formal wording. (FIX_RESOLVE) 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
|
|
||
| ## [1.11.0] - 2026-05-07 | ||
|
|
||
| ### Added | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>net6.0</TargetFramework> | ||
| <TargetFramework>net8.0</TargetFramework> | ||
| <IsPackable>false</IsPackable> | ||
| </PropertyGroup> | ||
|
Comment on lines
3
to
6
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Required project metadata is still missing from this The new 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
|
|
||
|
|
@@ -18,6 +18,7 @@ | |
| <PrivateAssets>all</PrivateAssets> | ||
| <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||
| </PackageReference> | ||
| <PackageReference Include="Testcontainers" Version="4.12.0" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| using System; | ||
| using System.Net.Http; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using DotNet.Testcontainers.Builders; | ||
| using DotNet.Testcontainers.Containers; | ||
|
|
||
| namespace Frends.HTTP.Request.Tests; | ||
|
|
||
| internal static class HttpBinContainer | ||
| { | ||
| private static readonly SemaphoreSlim StartStopLock = new(1, 1); | ||
| private static IContainer container; | ||
| private static string baseUrl; | ||
|
|
||
| public static string BaseUrl => baseUrl ?? throw new InvalidOperationException("HTTP test container has not been started."); | ||
|
|
||
| public static async Task StartAsync() | ||
| { | ||
| if (container != null) | ||
| return; | ||
|
|
||
| await StartStopLock.WaitAsync().ConfigureAwait(false); | ||
| try | ||
| { | ||
| if (container != null) | ||
| return; | ||
|
|
||
| container = new ContainerBuilder("kennethreitz/httpbin:latest") | ||
| .WithPortBinding(80, true) | ||
| .WithCleanUp(true) | ||
| .Build(); | ||
|
|
||
| await container.StartAsync().ConfigureAwait(false); | ||
|
|
||
| var mappedPort = container.GetMappedPublicPort(80); | ||
| baseUrl = $"http://localhost:{mappedPort}"; | ||
|
|
||
| await WaitUntilReadyAsync(baseUrl).ConfigureAwait(false); | ||
| } | ||
| finally | ||
| { | ||
| StartStopLock.Release(); | ||
| } | ||
| } | ||
|
|
||
| public static async Task StopAsync() | ||
| { | ||
| await StartStopLock.WaitAsync().ConfigureAwait(false); | ||
| try | ||
| { | ||
| if (container == null) | ||
| return; | ||
|
|
||
| await container.DisposeAsync().ConfigureAwait(false); | ||
| container = null; | ||
| baseUrl = null; | ||
| } | ||
| finally | ||
| { | ||
| StartStopLock.Release(); | ||
| } | ||
| } | ||
|
|
||
| private static async Task WaitUntilReadyAsync(string baseUrl) | ||
| { | ||
| using var client = new HttpClient(); | ||
|
|
||
| for (var attempt = 0; attempt < 30; attempt++) | ||
| { | ||
| try | ||
| { | ||
| var response = await client.GetAsync($"{baseUrl}/status/200").ConfigureAwait(false); | ||
| if (response.IsSuccessStatusCode) | ||
| return; | ||
| } | ||
| catch | ||
| { | ||
| // Container may still be starting up. | ||
| } | ||
|
|
||
| await Task.Delay(TimeSpan.FromMilliseconds(500)).ConfigureAwait(false); | ||
| } | ||
|
|
||
| throw new InvalidOperationException("The HTTP test container did not become ready in time."); | ||
| } | ||
| } | ||
|
|
||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,7 +15,6 @@ | |
| using System.Runtime.CompilerServices; | ||
| using System.Diagnostics.CodeAnalysis; | ||
| using System.Security.Cryptography.X509Certificates; | ||
| using System.Security.Cryptography; | ||
| using Frends.HTTP.Request.Definitions; | ||
|
|
||
| [assembly: InternalsVisibleTo("Frends.HTTP.Request.Tests")] | ||
|
|
@@ -34,12 +33,6 @@ public static class HTTP | |
| 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<X509Certificate2>(); | ||
|
|
||
|
|
||
| internal static void ClearClientCache() | ||
|
|
@@ -66,9 +59,12 @@ public static async Task<Result> Request( | |
| CancellationToken cancellationToken | ||
| ) | ||
| { | ||
| HttpClient httpClient = null; | ||
| HttpContent httpContent = null; | ||
|
|
||
| try | ||
| { | ||
| if (string.IsNullOrEmpty(input.Url)) throw new ArgumentNullException("Url can not be empty."); | ||
| if (string.IsNullOrEmpty(input.Url)) throw new ArgumentNullException(nameof(input), "Url can not be empty."); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Throw against the actual invalid argument here. For an empty URL, the invalid member is 🤖 Prompt for AI Agents |
||
|
|
||
| httpClient = GetHttpClientForOptions(options); | ||
| var headers = GetHeaderDictionary(input.Headers, options); | ||
|
|
@@ -89,12 +85,10 @@ CancellationToken cancellationToken | |
| switch (input.ResultMethod) | ||
| { | ||
| case ReturnFormat.String: | ||
| var hbody = responseMessage.Content != null | ||
| ? await responseMessage.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false) | ||
| : null; | ||
| var hbody = await responseMessage.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); | ||
| var hstatusCode = (int)responseMessage.StatusCode; | ||
| var hheaders = | ||
| GetResponseHeaderDictionary(responseMessage.Headers, responseMessage.Content?.Headers); | ||
| GetResponseHeaderDictionary(responseMessage.Headers, responseMessage.Content.Headers); | ||
| response = new Result(hbody, hheaders, hstatusCode); | ||
|
|
||
| break; | ||
|
|
@@ -123,15 +117,11 @@ CancellationToken cancellationToken | |
| } | ||
| finally | ||
| { | ||
| httpResponseMessage?.Dispose(); | ||
| httpRequestMessage?.Dispose(); | ||
| httpContent?.Dispose(); | ||
|
|
||
| if (!options.CacheHttpClient) | ||
| { | ||
| foreach (var cert in certificates) cert?.Dispose(); | ||
| httpClient?.Dispose(); | ||
| httpClientHandler?.Dispose(); | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -237,9 +227,10 @@ private static HttpClient GetHttpClientForOptions(Options options) | |
| } | ||
| } | ||
|
|
||
| httpClientHandler = new HttpClientHandler(); | ||
| var httpClientHandler = new HttpClientHandler(); | ||
| X509Certificate2[] certificates = Array.Empty<X509Certificate2>(); | ||
| httpClientHandler.SetHandlerSettingsBasedOnOptions(options, ref certificates); | ||
| httpClient = new HttpClient(httpClientHandler); | ||
| var httpClient = new HttpClient(httpClientHandler); | ||
| httpClient.SetDefaultRequestHeadersBasedOnOptions(options); | ||
|
|
||
| if (cacheKey != null) ClientCache.Add(cacheKey, httpClient, CachePolicy); | ||
|
|
@@ -267,7 +258,7 @@ private static async Task<HttpResponseMessage> GetHttpRequestResponseAsync( | |
| { | ||
| cancellationToken.ThrowIfCancellationRequested(); | ||
|
|
||
| httpRequestMessage = new HttpRequestMessage(new HttpMethod(method), new Uri(url)); | ||
| var httpRequestMessage = new HttpRequestMessage(new HttpMethod(method), new Uri(url)); | ||
| httpRequestMessage.Content = content; | ||
|
|
||
| //Clear default headers | ||
|
|
@@ -291,6 +282,7 @@ private static async Task<HttpResponseMessage> GetHttpRequestResponseAsync( | |
| } | ||
| } | ||
|
|
||
| HttpResponseMessage httpResponseMessage; | ||
| try | ||
| { | ||
| httpResponseMessage = await client.SendAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false); | ||
|
|
@@ -303,13 +295,13 @@ private static async Task<HttpResponseMessage> GetHttpRequestResponseAsync( | |
| throw; | ||
| } | ||
|
|
||
| // Cancellation is from inside of the request, mostly likely a timeout | ||
| // Cancellation is from inside the request, mostly likely a timeout | ||
| throw new Exception("HttpRequest was canceled, most likely due to a timeout.", canceledException); | ||
|
Comment on lines
+298
to
299
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Preserve a timeout-specific exception type. Wrapping an internal timeout as plain 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
|
|
||
| // 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) | ||
| if (options.AllowInvalidResponseContentTypeCharSet && httpResponseMessage.Content.Headers.ContentType != null) | ||
| { | ||
| httpResponseMessage.Content.Headers.ContentType.CharSet = null; | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mutable reusable workflow refs in
.github/workflows/Request_build_and_test_on_main.ymland.github/workflows/Request_build_and_test_on_push.yml.Both files pin to
@mainfor external reusable workflows. The shared root cause is mutable upstream execution in CI; pin bothuses:references to immutable full commit SHAs.🧰 Tools
🪛 GitHub Check: CodeQL
[warning] 13-17: Workflow does not contain permissions
Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {{}}
🪛 zizmor (1.25.2)
[error] 13-13: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
🤖 Prompt for AI Agents
Source: Linters/SAST tools