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

Aldaviva / Unfucked / 23424173368

23 Mar 2026 06:17AM UTC coverage: 45.693% (+3.8%) from 41.876%
23424173368

push

github

Aldaviva
Restore ability to mock UnfuckedHttpHandler class not using interfaces by making TestableSendAsync(HttpRequestMessage,CancellationToken) virtual

646 of 1511 branches covered (42.75%)

3 of 12 new or added lines in 1 file covered. (25.0%)

2 existing lines in 2 files now uncovered.

1151 of 2519 relevant lines covered (45.69%)

177.46 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(() => typeof(HttpMessageInvoker).GetFields(BindingFlags.NonPublic | BindingFlags.Instance)
1✔
43
        .First(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 />
NEW
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) {
NEW
195
        if (disposing && !disposed) {
×
196
            disposed = true;
×
NEW
197
            if (disposeInnerHandler) {
×
NEW
198
                InnerHandler?.Dispose();
×
199
            }
200

NEW
201
            List<KeyValuePair<int, WeakReference<IUnfuckedHttpHandler>?>> evictions =
×
NEW
202
                HTTP_CLIENT_HANDLER_CACHE.Where(pair => pair.Value != null && (!pair.Value.TryGetTarget(out IUnfuckedHttpHandler? handler) || handler == this)).ToList();
×
NEW
203
            foreach (KeyValuePair<int, WeakReference<IUnfuckedHttpHandler>?> eviction in evictions) {
×
204
#if NET5_0_OR_GREATER
NEW
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
NEW
212
            wireLoggingMeterFactory?.Dispose();
×
213
#endif
214
        }
UNCOV
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
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => realHandler.FilterAndSendAsync(request, cancellationToken);
×
226

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