• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Aldaviva / Unfucked / 24047187514

06 Apr 2026 07:22PM UTC coverage: 43.559% (-1.7%) from 45.265%
24047187514

push

github

Aldaviva
Make lambdas static where possible for better performance. Use concrete types instead of interfaces where possible to avoid virtual calls, also for perceived performance. Add more extensions to easily create ConcurrentDictionaries with atomic mutable values. Make mutable value holders in ConcurrentDictionary not equatable except for reference equality to avoid instances getting lost in the dictionary when the value and therefore hashcode changes. Added overloads to ConcurrentDictionary addition-aware upserting to take a valueFactory argument, to allow callers to not have to create a closure for their valueFactory if they already had a value they wanted to pass to it. Removed usage of array pool when converting special URL characters to their URL-encoded form, because the arrays are short enough (4 bytes) that it's faster to allocate and collect with GC than to borrow and return to a pool.

645 of 1560 branches covered (41.35%)

65 of 148 new or added lines in 22 files covered. (43.92%)

86 existing lines in 7 files now uncovered.

1143 of 2624 relevant lines covered (43.56%)

177.96 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

19.12
/HTTP/UnfuckedHttpClient.cs
1
using System.Net.Http.Headers;
2
using System.Reflection;
3
using Unfucked.HTTP.Config;
4
using Unfucked.HTTP.Exceptions;
5
using Unfucked.HTTP.Serialization;
6
#if NET8_0_OR_GREATER
7
using Unfucked.HTTP.Filters;
8
#endif
9

10
namespace Unfucked.HTTP;
11

12
/// <summary>
13
/// Interface for <see cref="UnfuckedHttpClient"/>, an improved subclass of <see cref="HttpClient"/>.
14
/// </summary>
15
public interface IHttpClient: IDisposable {
16

17
    /// <summary>
18
    /// The HTTP message handler, such as an <see cref="UnfuckedHttpHandler"/>.
19
    /// </summary>
20
    IUnfuckedHttpHandler? Handler { get; }
21

22
    /// <summary>
23
    /// <para>Send an HTTP request using a nice parameterized options struct.</para>
24
    /// <para>Generally, this is called internally by the <see cref="IWebTarget"/> builder, which is more fluent (<c>HttpClient.Target(url).Get&lt;string&gt;()</c>, for example).</para>
25
    /// </summary>
26
    /// <param name="request">the HTTP verb, URL, headers, and optional body to send</param>
27
    /// <param name="cancellationToken">cancel the request</param>
28
    /// <returns>HTTP response, after the response headers only are read</returns>
29
    /// <exception cref="ProcessingException">the HTTP request timed out (<see cref="TimeoutException"/>) or threw an <see cref="HttpRequestException"/></exception>
30
    Task<HttpResponseMessage> SendAsync(HttpRequest request, CancellationToken cancellationToken = default);
31

32
}
33

34
/// <summary>
35
/// <para>An improved subclass of <see cref="HttpClient"/>.</para>
36
/// <para>Usage:</para>
37
/// <para><c>using HttpClient client = new UnfuckedHttpClient();
38
/// MyObject response = await client.Target(url).Get&lt;MyObject&gt;();</c></para>
39
/// </summary>
40
public class UnfuckedHttpClient: HttpClient, IHttpClient {
41

42
    private static readonly TimeSpan DEFAULT_TIMEOUT = new(0, 0, 30);
1✔
43

44
    private static readonly Lazy<(string name, Version version)?> USER_AGENT = new(static () => Assembly.GetEntryAssembly()?.GetName() is { Name: {} programName, Version: {} programVersion }
2!
45
        ? (programName, programVersion) : null, LazyThreadSafetyMode.PublicationOnly);
2✔
46

47
    /// <inheritdoc />
48
    public IUnfuckedHttpHandler? Handler { get; }
1✔
49

50
    /// <summary>
51
    /// <para>Create a new <see cref="UnfuckedHttpClient"/> with a default message handler and configuration.</para>
52
    /// <para>Includes a default 30 second response timeout, 10 second connect timeout, 1 hour connection pool lifetime, and user-agent header named after your program.</para>
53
    /// </summary>
54
    public UnfuckedHttpClient(): this((IUnfuckedHttpHandler) new UnfuckedHttpHandler()) {}
22✔
55

56
#pragma warning disable CS1574, CS1584, CS1581, CS1580
57
    // This is not a factory method because it lets us both pass a SocketsHttpHandler with custom properties like PooledConnectionLifetime, as well as init properties on the UnfuckedHttpClient like Timeout. If this were a factory method, init property accessors would not be available, and callers would have to set them later on a temporary variable which can't all fit in one expression.
58
    /// <summary>
59
    /// Create a new <see cref="UnfuckedHttpClient"/> instance with the given handler.
60
    /// </summary>
61
    /// <param name="handler">An <see cref="HttpMessageHandler"/> used to send requests, typically a <see cref="SocketsHttpHandler"/> with custom properties.</param>
62
    /// <param name="disposeHandler"><c>true</c> to dispose of <paramref name="handler"/> when this instance is disposed, or <c>false</c> to not dispose it.</param>
63
    public UnfuckedHttpClient(HttpMessageHandler handler, bool disposeHandler = true): this(handler as IUnfuckedHttpHandler ?? new UnfuckedHttpHandler(handler), disposeHandler) {}
×
64
#pragma warning restore CS1574, CS1584, CS1581, CS1580
65

66
    /// <summary>
67
    /// Create a new <see cref="UnfuckedHttpClient"/> instance with a new handler and the given <paramref name="configuration"/>.
68
    /// </summary>
69
    /// <param name="configuration">Properties, filters, and message body readers to use in the new instance.</param>
70
    public UnfuckedHttpClient(IClientConfig configuration): this((IUnfuckedHttpHandler) new UnfuckedHttpHandler(null, configuration)) {}
×
71

72
    /// <summary>
73
    /// Main constructor that other constructors and factory methods delegate to.
74
    /// </summary>
75
    /// <param name="unfuckedHandler"></param>
76
    /// <param name="disposeHandler"></param>
77
    private UnfuckedHttpClient(IUnfuckedHttpHandler unfuckedHandler, bool disposeHandler = true): base(unfuckedHandler as HttpMessageHandler ?? new IUnfuckedHttpHandlerWrapper(unfuckedHandler),
11!
78
        disposeHandler) {
11✔
79
        Handler = unfuckedHandler;
11✔
80
        Timeout = DEFAULT_TIMEOUT;
11✔
81
        if (USER_AGENT.Value is var (programName, programVersion)) {
11✔
82
            DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(programName, programVersion.ToString(4, true)));
11✔
83
        }
84
        UnfuckedHttpHandler.CacheClientHandler(this, unfuckedHandler);
11✔
85
    }
11✔
86

87
    /// <summary>
88
    /// Create a new <see cref="UnfuckedHttpClient"/> instance that uses the given <paramref name="unfuckedHandler"/> to send requests. This is mostly useful for testing where <paramref name="unfuckedHandler"/> is a mock. When it's a real <see cref="UnfuckedHttpHandler"/>, you can use the <see cref="UnfuckedHttpClient(HttpMessageHandler,bool)"/> constructor instead.
89
    /// </summary>
90
    /// <param name="unfuckedHandler">Handler used to send requests.</param>
91
    /// <param name="disposeHandler"><c>true</c> to dispose of <paramref name="unfuckedHandler"/> when this instance is disposed, or <c>false</c> to not dispose it.</param>
92
    /// <returns></returns>
93
    public static UnfuckedHttpClient Create(IUnfuckedHttpHandler unfuckedHandler, bool disposeHandler = true) => new(unfuckedHandler, disposeHandler);
×
94

95
    // This factory method is no longer constructors so DI gets less confused by the arguments, even though many are optional, to prevent it trying to inject a real HttpMessageHandler in a symmetric dependency. Microsoft.Extensions.DependencyInjection always picks the constructor overload with the most injectable arguments, but I want it to pick the no-arg constructor.
96
    /// <summary>
97
    /// Create a new <see cref="UnfuckedHttpClient"/> that copies the settings of an existing <see cref="HttpClient"/>.
98
    /// </summary>
99
    /// <param name="toClone"><see cref="HttpClient"/> to copy.</param>
100
    /// <returns>A new instance of an <see cref="UnfuckedHttpClient"/> with the same handler and configuration as <paramref name="toClone"/>.</returns>
101
    // ExceptionAdjustment: M:System.Net.Http.Headers.HttpHeaders.Add(System.String,System.Collections.Generic.IEnumerable{System.String}) -T:System.FormatException
102
    public static UnfuckedHttpClient Create(HttpClient toClone) {
103
        IUnfuckedHttpHandler newHandler;
104
        bool                 disposeHandler;
105
        if (toClone is UnfuckedHttpClient { Handler: {} h }) {
×
106
            newHandler     = h;
×
107
            disposeHandler = false; // we don't own it, toClone does
×
108
        } else {
109
            newHandler     = UnfuckedHttpHandler.Create(toClone);
×
110
            disposeHandler = true; // we own it, although it won't dispose toClone's inner handler because it wasn't created by the new UnfuckedHttpHandler
×
111
        }
112

113
        UnfuckedHttpClient newClient = new(newHandler, disposeHandler) {
×
114
            BaseAddress                  = toClone.BaseAddress,
×
115
            Timeout                      = toClone.Timeout,
×
116
            MaxResponseContentBufferSize = toClone.MaxResponseContentBufferSize,
×
117
#if NETCOREAPP3_0_OR_GREATER
×
118
            DefaultRequestVersion = toClone.DefaultRequestVersion,
×
119
            DefaultVersionPolicy = toClone.DefaultVersionPolicy
×
120
#endif
×
121
        };
×
122

123
        foreach (KeyValuePair<string, IEnumerable<string>> wrappedDefaultHeader in toClone.DefaultRequestHeaders) {
×
124
            newClient.DefaultRequestHeaders.Add(wrappedDefaultHeader.Key, wrappedDefaultHeader.Value);
×
125
        }
126

127
        return newClient;
×
128
    }
129

130
    /// <inheritdoc />
131
    public virtual Task<HttpResponseMessage> SendAsync(HttpRequest request, CancellationToken cancellationToken = default) {
132
#if NET8_0_OR_GREATER
133
        WireLogFilter.AsyncState.Value = new WireLogFilter.WireAsyncState();
×
134
#endif
135

136
        UnfuckedHttpRequestMessage req = new(request.Verb, request.Uri) {
×
137
            Content = request.Body,
×
138
            Config  = request.ClientConfig
×
139
        };
×
140

141
        if (req.Content is Entity.JsonHttpContent json) {
×
142
            json.ClientOptions ??= JsonBodyReader.DefaultJsonOptions;
×
143
        }
144

145
        try {
146
            foreach (KeyValuePair<string, string> header in request.Headers) {
×
147
                req.Headers.Add(header.Key, header.Value);
×
148
            }
149
        } catch (FormatException e) {
×
150
            throw new ProcessingException(e, HttpExceptionParams.FromRequest(req));
×
151
        }
152

153
        // Set wire logging AsyncLocal outside of this async method so it is available higher in the await chain when the response finishes
154
        return SendAsync(this, req, cancellationToken);
×
155
    }
156

157
    /// <exception cref="ProcessingException"></exception>
158
    internal static async Task<HttpResponseMessage> SendAsync(HttpClient client, UnfuckedHttpRequestMessage request, CancellationToken cancellationToken) {
159
        try {
160
            return await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
×
161
        } catch (OperationCanceledException e) {
162
            // Official documentation is wrong: .NET Framework throws a TaskCanceledException for an HTTP request timeout, not an HttpRequestException (https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.sendasync)
163
            TimeoutException cause = e.InnerException as TimeoutException ??
×
164
                new TimeoutException($"The request was canceled due to the configured {nameof(HttpClient)}.{nameof(Timeout)} of {client.Timeout.TotalSeconds} seconds elapsing.");
×
165
            throw new ProcessingException(cause, HttpExceptionParams.FromRequest(request));
×
166
        } catch (HttpRequestException e) {
×
167
            throw new ProcessingException(e.InnerException ?? e, HttpExceptionParams.FromRequest(request));
×
168
        } finally {
169
            request.Dispose();
×
170
        }
171
    }
×
172

173
}
174

175
internal sealed class HttpClientWrapper: IHttpClient {
176

177
    private readonly HttpClient realClient;
178

179
    public IUnfuckedHttpHandler? Handler { get; }
×
180

181
    private HttpClientWrapper(HttpClient realClient) {
×
182
        this.realClient = realClient;
×
183
        Handler         = UnfuckedHttpHandler.FindHandler(realClient);
×
184
    }
×
185

186
    public static IHttpClient Wrap(IHttpClient client) => client is HttpClient httpClient and not UnfuckedHttpClient ? new HttpClientWrapper(httpClient) : client;
×
187
    public static IHttpClient Wrap(HttpClient client) => client as UnfuckedHttpClient as IHttpClient ?? new HttpClientWrapper(client);
×
188

189
    /// <exception cref="ProcessingException"></exception>
190
    public Task<HttpResponseMessage> SendAsync(HttpRequest request, CancellationToken cancellationToken = default) {
191
        using UnfuckedHttpRequestMessage req = new(request);
×
192

193
        try {
NEW
194
            foreach (IGrouping<string, string> header in request.Headers.GroupBy(static pair => pair.Key, static pair => pair.Value, StringComparer.OrdinalIgnoreCase)) {
×
195
                req.Headers.Add(header.Key, header);
×
196
            }
197
        } catch (FormatException e) {
×
198
            throw new ProcessingException(e, HttpExceptionParams.FromRequest(req));
×
199
        }
200
        return UnfuckedHttpClient.SendAsync(realClient, new UnfuckedHttpRequestMessage(request), cancellationToken);
×
201
    }
×
202

203
    public void Dispose() {}
×
204

205
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc