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

Aldaviva / Unfucked / 19267488260

11 Nov 2025 01:23PM UTC coverage: 47.79% (+0.04%) from 47.751%
19267488260

push

github

Aldaviva
Enable automatic HTTP response body decompression by default

630 of 1655 branches covered (38.07%)

10 of 15 new or added lines in 3 files covered. (66.67%)

78 existing lines in 5 files now uncovered.

1146 of 2398 relevant lines covered (47.79%)

51.05 hits per line

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

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

9
namespace Unfucked.HTTP;
10

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

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

21
    /// <summary>
22
    /// <para>Send an HTTP request using a nice parameterized options struct.</para>
23
    /// <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>
24
    /// </summary>
25
    /// <param name="request">the HTTP verb, URL, headers, and optional body to send</param>
26
    /// <param name="cancellationToken">cancel the request</param>
27
    /// <returns>HTTP response, after the response headers only are read</returns>
28
    /// <exception cref="ProcessingException">the HTTP request timed out (<see cref="TimeoutException"/>) or threw an <see cref="HttpRequestException"/></exception>
29
    Task<HttpResponseMessage> SendAsync(HttpRequest request, CancellationToken cancellationToken = default);
30

31
}
32

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

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

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

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

49
    /// <summary>
50
    /// <para>Create a new <see cref="UnfuckedHttpClient"/> with a default message handler and configuration.</para>
51
    /// <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>
52
    /// </summary>
53
    public UnfuckedHttpClient(): this((IUnfuckedHttpHandler) new UnfuckedHttpHandler()) {}
22✔
54

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

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

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

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

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

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

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

UNCOV
124
        return newClient;
×
125
    }
126

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

133
        UnfuckedHttpRequestMessage req = new(request.Verb, request.Uri) {
×
134
            Content = request.Body,
×
135
            Config  = request.ClientConfig
×
UNCOV
136
        };
×
137
        try {
138
            foreach (KeyValuePair<string, string> header in request.Headers) {
×
UNCOV
139
                req.Headers.Add(header.Key, header.Value);
×
140
            }
UNCOV
141
        } catch (FormatException e) {
×
142
            throw new ProcessingException(e, HttpExceptionParams.FromRequest(req));
×
143
        }
144

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

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

165
}
166

167
internal class HttpClientWrapper: IHttpClient {
168

169
    private readonly HttpClient realClient;
170

UNCOV
171
    public IUnfuckedHttpHandler? Handler { get; }
×
172

UNCOV
173
    private HttpClientWrapper(HttpClient realClient) {
×
UNCOV
174
        this.realClient = realClient;
×
UNCOV
175
        Handler         = UnfuckedHttpHandler.FindHandler(realClient);
×
UNCOV
176
    }
×
177

UNCOV
178
    public static IHttpClient Wrap(IHttpClient client) => client is HttpClient httpClient and not UnfuckedHttpClient ? new HttpClientWrapper(httpClient) : client;
×
UNCOV
179
    public static IHttpClient Wrap(HttpClient client) => client as UnfuckedHttpClient as IHttpClient ?? new HttpClientWrapper(client);
×
180

181
    /// <exception cref="ProcessingException"></exception>
182
    public Task<HttpResponseMessage> SendAsync(HttpRequest request, CancellationToken cancellationToken = default) {
UNCOV
183
        using UnfuckedHttpRequestMessage req = new(request);
×
184

185
        try {
UNCOV
186
            foreach (IGrouping<string, string> header in request.Headers.GroupBy(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase)) {
×
UNCOV
187
                req.Headers.Add(header.Key, header);
×
188
            }
UNCOV
189
        } catch (FormatException e) {
×
UNCOV
190
            throw new ProcessingException(e, HttpExceptionParams.FromRequest(req));
×
191
        }
UNCOV
192
        return UnfuckedHttpClient.SendAsync(realClient, new UnfuckedHttpRequestMessage(request), cancellationToken);
×
UNCOV
193
    }
×
194

195
    public void Dispose() {
UNCOV
196
        GC.SuppressFinalize(this);
×
UNCOV
197
    }
×
198

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