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

Aldaviva / Unfucked / 23378203323

21 Mar 2026 10:59AM UTC coverage: 35.442% (-11.7%) from 47.183%
23378203323

push

github

Aldaviva
Seal all possible classes for allegedly higher performance, since they weren't actually subclassable anyway due to C# not making methods virtual by default. If this change does more harm than good, blame Stephen Toub.

573 of 1629 branches covered (35.17%)

14 of 72 new or added lines in 15 files covered. (19.44%)

488 existing lines in 30 files now uncovered.

975 of 2751 relevant lines covered (35.44%)

162.06 hits per line

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

0.0
/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 sealed class UnfuckedHttpClient: HttpClient, IHttpClient {
41

UNCOV
42
    private static readonly TimeSpan DEFAULT_TIMEOUT = new(0, 0, 30);
×
43

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

47
    /// <inheritdoc />
UNCOV
48
    public IUnfuckedHttpHandler? Handler { get; }
×
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>
UNCOV
54
    public UnfuckedHttpClient(): this((IUnfuckedHttpHandler) new UnfuckedHttpHandler()) {}
×
55

56
    // 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.
57
    /// <summary>
58
    /// Create a new <see cref="UnfuckedHttpClient"/> instance with the given handler.
59
    /// </summary>
60
    /// <param name="handler">An <see cref="HttpMessageHandler"/> used to send requests, typically a <see cref="SocketsHttpHandler"/> with custom properties.</param>
61
    /// <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>
UNCOV
62
    public UnfuckedHttpClient(HttpMessageHandler handler, bool disposeHandler = true): this(handler as IUnfuckedHttpHandler ?? new UnfuckedHttpHandler(handler), disposeHandler) {}
×
63

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

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

85
    /// <summary>
86
    /// 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.
87
    /// </summary>
88
    /// <param name="unfuckedHandler">Handler used to send requests.</param>
89
    /// <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>
90
    /// <returns></returns>
UNCOV
91
    public static UnfuckedHttpClient Create(IUnfuckedHttpHandler unfuckedHandler, bool disposeHandler = true) => new(unfuckedHandler, disposeHandler);
×
92

93
    // 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.
94
    /// <summary>
95
    /// Create a new <see cref="UnfuckedHttpClient"/> that copies the settings of an existing <see cref="HttpClient"/>.
96
    /// </summary>
97
    /// <param name="toClone"><see cref="HttpClient"/> to copy.</param>
98
    /// <returns>A new instance of an <see cref="UnfuckedHttpClient"/> with the same handler and configuration as <paramref name="toClone"/>.</returns>
99
    // ExceptionAdjustment: M:System.Net.Http.Headers.HttpHeaders.Add(System.String,System.Collections.Generic.IEnumerable{System.String}) -T:System.FormatException
100
    public static UnfuckedHttpClient Create(HttpClient toClone) {
101
        IUnfuckedHttpHandler newHandler;
102
        bool                 disposeHandler;
103
        if (toClone is UnfuckedHttpClient { Handler: {} h }) {
×
104
            newHandler     = h;
×
UNCOV
105
            disposeHandler = false; // we don't own it, toClone does
×
106
        } else {
107
            newHandler     = UnfuckedHttpHandler.Create(toClone);
×
UNCOV
108
            disposeHandler = true; // we own it, although it won't dispose toClone's inner handler because it wasn't created by the new UnfuckedHttpHandler
×
109
        }
110

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

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

UNCOV
125
        return newClient;
×
126
    }
127

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

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

139
        if (req.Content is Entity.JsonHttpContent json) {
×
UNCOV
140
            json.ClientOptions ??= JsonBodyReader.DEFAULT_JSON_OPTIONS;
×
141
        }
142

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

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

155
    /// <exception cref="ProcessingException"></exception>
156
    internal static async Task<HttpResponseMessage> SendAsync(HttpClient client, UnfuckedHttpRequestMessage request, CancellationToken cancellationToken) {
157
        try {
158
            return await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
×
159
        } catch (OperationCanceledException e) {
160
            // 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)
161
            TimeoutException cause = e.InnerException as TimeoutException ??
×
UNCOV
162
                new TimeoutException($"The request was canceled due to the configured {nameof(HttpClient)}.{nameof(Timeout)} of {client.Timeout.TotalSeconds} seconds elapsing.");
×
163
            throw new ProcessingException(cause, HttpExceptionParams.FromRequest(request));
×
UNCOV
164
        } catch (HttpRequestException e) {
×
UNCOV
165
            throw new ProcessingException(e.InnerException ?? e, HttpExceptionParams.FromRequest(request));
×
166
        } finally {
UNCOV
167
            request.Dispose();
×
168
        }
UNCOV
169
    }
×
170

171
}
172

173
internal sealed class HttpClientWrapper: IHttpClient {
174

175
    private readonly HttpClient realClient;
176

UNCOV
177
    public IUnfuckedHttpHandler? Handler { get; }
×
178

179
    private HttpClientWrapper(HttpClient realClient) {
×
UNCOV
180
        this.realClient = realClient;
×
UNCOV
181
        Handler         = UnfuckedHttpHandler.FindHandler(realClient);
×
UNCOV
182
    }
×
183

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

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

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

NEW
201
    public void Dispose() {}
×
202

203
}
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