• 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

31.4
/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]
UNCOV
59
    public IReadOnlyList<ClientRequestFilter> RequestFilters => ClientConfig.RequestFilters;
×
60

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

65
    /// <inheritdoc />
66
    [Pure]
UNCOV
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 (!disposed) {
×
196
            disposed = true;
×
197
            if (disposing) {
×
198
                if (disposeInnerHandler) {
×
199
                    InnerHandler?.Dispose();
×
200
                }
201

UNCOV
202
                List<KeyValuePair<int, WeakReference<IUnfuckedHttpHandler>?>> evictions =
×
UNCOV
203
                    HTTP_CLIENT_HANDLER_CACHE.Where(pair => pair.Value != null && (!pair.Value.TryGetTarget(out IUnfuckedHttpHandler? handler) || handler == this)).ToList();
×
UNCOV
204
                foreach (KeyValuePair<int, WeakReference<IUnfuckedHttpHandler>?> eviction in evictions) {
×
205
#if NET5_0_OR_GREATER
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
213
                wireLoggingMeterFactory?.Dispose();
×
214
#endif
215
            }
216
        }
UNCOV
217
        base.Dispose(disposing);
×
UNCOV
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>
UNCOV
225
internal class IUnfuckedHttpHandlerWrapper(IUnfuckedHttpHandler realHandler): HttpMessageHandler {
×
226

UNCOV
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