• 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

31.03
/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 class UnfuckedHttpHandler: DelegatingHandler, IUnfuckedHttpHandler {
39

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

42
    private static readonly Lazy<FieldInfo> HTTP_CLIENT_HANDLER_FIELD = new(static () => typeof(HttpMessageInvoker).GetFields(BindingFlags.NonPublic | BindingFlags.Instance)
1✔
43
        .First(static field => field.FieldType == typeof(HttpMessageHandler)), LazyThreadSafetyMode.PublicationOnly);
1✔
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 />
55
    public IClientConfig ClientConfig { get; private set; }
23✔
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
     */
74
    public UnfuckedHttpHandler(): this(null) {}
22✔
75

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

96
#if NET8_0_OR_GREATER
97
        if (innerHandler == null) {
11✔
98
            wireLoggingMeterFactory = ((SocketsHttpHandler) InnerHandler!).MeterFactory;
11✔
99
        }
100
#endif
101
    }
11✔
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) =>
127
        HTTP_CLIENT_HANDLER_CACHE[client.GetHashCode()] = handler is null ? null : new WeakReference<IUnfuckedHttpHandler>(handler);
11!
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 />
171
    public virtual 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 (disposing && !disposed) {
×
196
            disposed = true;
×
197
            if (disposeInnerHandler) {
×
198
                InnerHandler?.Dispose();
×
199
            }
200

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

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

218
}
219

220
/// <summary>
221
/// 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.
222
/// </summary>
223
internal sealed class IUnfuckedHttpHandlerWrapper(IUnfuckedHttpHandler realHandler): HttpMessageHandler {
×
224

225
    /// <exception cref="ProcessingException"></exception>
UNCOV
226
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => realHandler.FilterAndSendAsync(request, cancellationToken);
×
227

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