• 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/UnfuckedHttpHandler.cs
1
using System.Collections.Concurrent;
2
using System.Diagnostics.Contracts;
3
using System.Net;
4
using System.Reflection;
5
using Unfucked.HTTP.Config;
6
using Unfucked.HTTP.Exceptions;
7
using Unfucked.HTTP.Filters;
8
using Unfucked.HTTP.Serialization;
9
#if NET8_0_OR_GREATER
10
using System.Diagnostics.Metrics;
11
#endif
12

13
namespace Unfucked.HTTP;
14

15
public interface IUnfuckedHttpHandler: Configurable<IUnfuckedHttpHandler> {
16

17
    /// <inheritdoc cref="DelegatingHandler.InnerHandler" />
18
    HttpMessageHandler? InnerHandler { get; }
19

20
    /// <summary>
21
    /// HTTP client configuration, including properties, request and response filters, and message body readers
22
    /// </summary>
23
    IClientConfig ClientConfig { get; }
24

25
    /// <summary>
26
    /// This is the method to mock/fake/stub/spy if you want to inspect HTTP requests and return fake responses instead of real ones.
27
    /// </summary>
28
    Task<HttpResponseMessage> TestableSendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
29

30
    /// <summary>
31
    /// This should just delegate to <c>UnfuckedHttpHandler.SendAsync</c>, it's only here because the method was originally only specified on a superclass, not an interface.
32
    /// </summary>
33
    /// <exception cref="ProcessingException"></exception>
34
    Task<HttpResponseMessage> FilterAndSendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
35

36
}
37

38
public sealed class UnfuckedHttpHandler: DelegatingHandler, IUnfuckedHttpHandler {
39

UNCOV
40
    private static readonly ConcurrentDictionary<int, WeakReference<IUnfuckedHttpHandler>?> HTTP_CLIENT_HANDLER_CACHE = new();
×
41

UNCOV
42
    private static readonly Lazy<FieldInfo> HTTP_CLIENT_HANDLER_FIELD = new(() => typeof(HttpMessageInvoker).GetFields(BindingFlags.NonPublic | BindingFlags.Instance)
×
UNCOV
43
        .First(field => field.FieldType == typeof(HttpMessageHandler)), LazyThreadSafetyMode.PublicationOnly);
×
44

45
    private readonly FilterContext baseFilterContext;
46
    private readonly bool          disposeInnerHandler;
47

48
#if NET8_0_OR_GREATER
49
    private readonly IMeterFactory? wireLoggingMeterFactory;
50
#endif
51

52
    private bool disposed;
53

54
    /// <inheritdoc />
UNCOV
55
    public IClientConfig ClientConfig { get; private set; }
×
56

57
    /// <inheritdoc />
58
    [Pure]
59
    public IReadOnlyList<ClientRequestFilter> RequestFilters => ClientConfig.RequestFilters;
×
60

61
    /// <inheritdoc />
62
    [Pure]
63
    public IReadOnlyList<ClientResponseFilter> ResponseFilters => ClientConfig.ResponseFilters;
×
64

65
    /// <inheritdoc />
66
    [Pure]
67
    public IEnumerable<MessageBodyReader> MessageBodyReaders => ClientConfig.MessageBodyReaders;
×
68

69
    /*
70
     * No-argument constructor overload lets FakeItEasy call this real constructor, which makes ClientConfig not a fake so registering JSON options aren't ignored, which would cause confusing errors
71
     * at test runtime. Default values for other constructor below wouldn't have been called by FakeItEasy. This avoids having to remember to call
72
     * options.WithArgumentsForConstructor(() => new UnfuckedHttpHandler(null, null)) when creating the fake.
73
     */
UNCOV
74
    public UnfuckedHttpHandler(): this(null) {}
×
75

76
    // HttpClientHandler automatically uses SocketsHttpHandler on .NET Core ≥ 2.1, or HttpClientHandler otherwise
UNCOV
77
    public UnfuckedHttpHandler(HttpMessageHandler? innerHandler = null, IClientConfig? configuration = null): base(innerHandler ??
×
UNCOV
78
#if NETCOREAPP2_1_OR_GREATER
×
UNCOV
79
        new SocketsHttpHandler {
×
UNCOV
80
            PooledConnectionLifetime = TimeSpan.FromHours(1),
×
UNCOV
81
            ConnectTimeout           = TimeSpan.FromSeconds(10),
×
UNCOV
82
            AutomaticDecompression   = DecompressionMethods.All,
×
UNCOV
83
            // MaxConnectionsPerServer defaults to MAX_INT, so we don't need to increase it here
×
UNCOV
84
#if NET8_0_OR_GREATER
×
UNCOV
85
            MeterFactory = new WireLogFilter.WireLoggingMeterFactory()
×
UNCOV
86
#endif
×
UNCOV
87
        }
×
UNCOV
88
#else
×
UNCOV
89
        new HttpClientHandler {AutomaticDecompression = DecompressionMethods.GZip|DecompressionMethods.Deflate}
×
UNCOV
90
#endif
×
UNCOV
91
    ) {
×
UNCOV
92
        ClientConfig        = configuration ?? new ClientConfig();
×
UNCOV
93
        baseFilterContext   = new FilterContext(this, ClientConfig);
×
UNCOV
94
        disposeInnerHandler = innerHandler == null;
×
95

96
#if NET8_0_OR_GREATER
UNCOV
97
        if (innerHandler == null) {
×
UNCOV
98
            wireLoggingMeterFactory = ((SocketsHttpHandler) InnerHandler!).MeterFactory;
×
99
        }
100
#endif
UNCOV
101
    }
×
102

103
    public static UnfuckedHttpHandler Create(HttpClient toClone, IClientConfig? configuration = null) =>
104
        new((HttpMessageHandler) HTTP_CLIENT_HANDLER_FIELD.Value.GetValue(toClone)!, configuration);
×
105

106
    internal static IUnfuckedHttpHandler? FindHandler(HttpClient httpClient) {
107
        if (httpClient is IHttpClient client) {
×
108
            return client.Handler;
×
109
        }
110

111
        IUnfuckedHttpHandler? handler = null;
×
112
        if (!HTTP_CLIENT_HANDLER_CACHE.TryGetValue(httpClient.GetHashCode(), out WeakReference<IUnfuckedHttpHandler>? handlerHolder) || !(handlerHolder?.TryGetTarget(out handler) ?? true)) {
×
113
            handler = findDescendantUnfuckedHandler((HttpMessageHandler?) HTTP_CLIENT_HANDLER_FIELD.Value.GetValue(httpClient));
×
114
            CacheClientHandler(httpClient, handler);
×
115
        }
116

117
        return handler;
×
118

119
        static UnfuckedHttpHandler? findDescendantUnfuckedHandler(HttpMessageHandler? parent) => parent switch {
×
120
            UnfuckedHttpHandler f => f,
×
121
            DelegatingHandler d   => findDescendantUnfuckedHandler(d.InnerHandler),
×
122
            _                     => null
×
123
        };
×
124
    }
125

126
    internal static void CacheClientHandler(HttpClient client, IUnfuckedHttpHandler? handler) =>
UNCOV
127
        HTTP_CLIENT_HANDLER_CACHE[client.GetHashCode()] = handler is null ? null : new WeakReference<IUnfuckedHttpHandler>(handler);
×
128

129
    /// <inheritdoc />
130
    public Task<HttpResponseMessage> FilterAndSendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => SendAsync(request, cancellationToken);
×
131

132
    /// <inheritdoc />
133
    /// <exception cref="ProcessingException">filter error</exception>
134
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
135
        IClientConfig? config        = (request as UnfuckedHttpRequestMessage)?.Config;
×
136
        FilterContext  filterContext = baseFilterContext with { Configuration = config ?? baseFilterContext.Configuration };
×
137

138
        try {
139
            foreach (ClientRequestFilter requestFilter in config?.RequestFilters ?? RequestFilters) {
×
140
                HttpRequestMessage newRequest = await requestFilter.Filter(request, filterContext, cancellationToken).ConfigureAwait(false);
×
141
                if (request != newRequest) {
×
142
                    request.Dispose();
×
143
                    request = newRequest;
×
144
                }
145
            }
146
        } catch (ProcessingException) {
×
147
            request.Dispose();
×
148
            throw;
×
149
        }
150

151
        HttpResponseMessage response = await TestableSendAsync(request, cancellationToken).ConfigureAwait(false);
×
152

153
        try {
154
            foreach (ClientResponseFilter responseFilter in config?.ResponseFilters ?? ResponseFilters) {
×
155
                HttpResponseMessage newResponse = await responseFilter.Filter(response, filterContext, cancellationToken).ConfigureAwait(false);
×
156
                if (response != newResponse) {
×
157
                    response.Dispose();
×
158
                    response = newResponse;
×
159
                }
160
            }
161
        } catch (ProcessingException) {
×
162
            request.Dispose();
×
163
            response.Dispose();
×
164
            throw;
×
165
        }
166

167
        return response;
×
168
    }
×
169

170
    /// <inheritdoc />
NEW
171
    public Task<HttpResponseMessage> TestableSendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => base.SendAsync(request, cancellationToken);
×
172

173
    private UnfuckedHttpHandler With(IClientConfig newConfig) {
174
        ClientConfig = newConfig;
×
175
        return this;
×
176
    }
177

178
    public IUnfuckedHttpHandler Register(Registrable registrable) => With(ClientConfig.Register(registrable));
×
179

180
    public IUnfuckedHttpHandler Register<Option>(Registrable<Option> registrable, Option registrationOption) => With(ClientConfig.Register(registrable, registrationOption));
×
181

182
    public IUnfuckedHttpHandler Property<T>(PropertyKey<T> key, T? value) where T: notnull => With(ClientConfig.Property(key, value));
×
183

184
    [Pure]
185
    public bool Property<T>(PropertyKey<T> key,
186
#if !NETSTANDARD2_0
187
                            [NotNullWhen(true)]
188
#endif
189
                            out T? existingValue) where T: notnull => ClientConfig.Property(key, out existingValue);
×
190

191
    [Pure]
192
    IUnfuckedHttpHandler Configurable<IUnfuckedHttpHandler>.Property<T>(PropertyKey<T> key, T? newValue) where T: default => Property(key, newValue);
×
193

194
    protected override void Dispose(bool disposing) {
195
        if (!disposed) {
×
196
            disposed = true;
×
197
            if (disposing) {
×
198
                if (disposeInnerHandler) {
×
199
                    InnerHandler?.Dispose();
×
200
                }
201

202
                List<KeyValuePair<int, WeakReference<IUnfuckedHttpHandler>?>> evictions =
×
203
                    HTTP_CLIENT_HANDLER_CACHE.Where(pair => pair.Value != null && (!pair.Value.TryGetTarget(out IUnfuckedHttpHandler? handler) || handler == this)).ToList();
×
204
                foreach (KeyValuePair<int, WeakReference<IUnfuckedHttpHandler>?> eviction in evictions) {
×
205
#if NET5_0_OR_GREATER
UNCOV
206
                    HTTP_CLIENT_HANDLER_CACHE.TryRemove(eviction);
×
207
#else
208
                    HTTP_CLIENT_HANDLER_CACHE.TryRemove(eviction.Key, out _);
209
#endif
210
                }
211

212
#if NET8_0_OR_GREATER
UNCOV
213
                wireLoggingMeterFactory?.Dispose();
×
214
#endif
215
            }
216
        }
217
        base.Dispose(disposing);
×
218
    }
×
219

220
}
221

222
/// <summary>
223
/// This is used when a consumer passes an IUnfuckedHttpHandler to an UnfuckedHttpClient constructor. Just because it implements IUnfuckedHttpHandler doesn't mean it's a subclass of HttpMessageHandler, and Microsoft stupidly decided to never use interfaces for anything. This class is an adapter that actually has the superclass needed to be used as an HttpMessageHandler.
224
/// </summary>
NEW
225
internal sealed class IUnfuckedHttpHandlerWrapper(IUnfuckedHttpHandler realHandler): HttpMessageHandler {
×
226

227
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => realHandler.FilterAndSendAsync(request, cancellationToken);
×
228

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