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

Aldaviva / Unfucked / 30068203755

24 Jul 2026 04:44AM UTC coverage: 43.974% (+0.04%) from 43.937%
30068203755

push

github

Aldaviva
HTTP: try to use a client or request configured JSON serializer when sending a request, not just when receiving a response

666 of 1907 branches covered (34.92%)

0 of 4 new or added lines in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

1164 of 2647 relevant lines covered (43.97%)

176.63 hits per line

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

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

11
namespace Unfucked.HTTP;
12

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

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

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

33
}
34

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

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

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

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

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

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

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

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

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

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

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

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

128
        return newClient;
×
129
    }
130

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

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

142
        if (req.Content is Entity.JsonHttpContent json) {
×
NEW
143
            json.ClientOptions ??= request.ClientConfig?.Property(PropertyKey.JsonSerializerOptions, out JsonSerializerOptions? requestJsonOptions) ?? false
×
NEW
144
                ? requestJsonOptions : JsonBodyReader.DefaultJsonOptions;
×
145
        }
146

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

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

159
    /// <exception cref="ProcessingException"></exception>
160
    internal static async Task<HttpResponseMessage> SendAsync(HttpClient client, UnfuckedHttpRequestMessage request, CancellationToken cancellationToken) {
161
        try {
162
            return await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
×
UNCOV
163
        } catch (OperationCanceledException e) {
×
164
            // 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)
NEW
165
            throw new ProcessingException(e.InnerException as TimeoutException as Exception ?? e, 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 {
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 TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc