Skip to content

Commit

Permalink
#769 use SocketsHttpHandler on supported platforms
Browse files Browse the repository at this point in the history
  • Loading branch information
Todd committed Oct 20, 2023
1 parent e9fbfbf commit d5ff66e
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 48 deletions.
50 changes: 35 additions & 15 deletions src/Flurl.Http/Configuration/FlurlClientBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ public interface IFlurlClientBuilder
/// <summary>
/// Configure the inner-most HttpMessageHandler associated with this IFlurlClient.
/// </summary>
#if NETCOREAPP2_1_OR_GREATER
IFlurlClientBuilder ConfigureInnerHandler(Action<SocketsHttpHandler> configAction);
#else
IFlurlClientBuilder ConfigureInnerHandler(Action<HttpClientHandler> configAction);
#endif

/// <summary>
/// Add a provided DelegatingHandler to the IFlurlClient.
Expand All @@ -44,9 +48,13 @@ public class FlurlClientBuilder : IFlurlClientBuilder
private readonly IFlurlClientFactory _factory;
private readonly string _baseUrl;
private readonly List<Func<DelegatingHandler>> _addMiddleware = new();
private readonly List<Action<HttpClient>> _configClient = new();
private readonly List<Action<HttpClientHandler>> _configHandler = new();
private readonly List<Action<FlurlHttpSettings>> _configSettings = new();
private readonly List<Action<HttpClient>> _configClient = new();
#if NETCOREAPP2_1_OR_GREATER
private readonly HandlerBuilder<SocketsHttpHandler> _handlerBuilder = new();
#else
private readonly HandlerBuilder<HttpClientHandler> _handlerBuilder = new();
#endif

/// <summary>
/// Creates a new FlurlClientBuilder.
Expand Down Expand Up @@ -80,24 +88,20 @@ public IFlurlClientBuilder ConfigureHttpClient(Action<HttpClient> configAction)
}

/// <inheritdoc />
#if NETCOREAPP2_1_OR_GREATER
public IFlurlClientBuilder ConfigureInnerHandler(Action<SocketsHttpHandler> configAction) {
#else
public IFlurlClientBuilder ConfigureInnerHandler(Action<HttpClientHandler> configAction) {
_configHandler.Add(configAction);
#endif
_handlerBuilder.Configs.Add(configAction);
return this;
}

/// <inheritdoc />
public IFlurlClient Build() {
var innerHandler = _factory.CreateInnerHandler();
foreach (var config in _configHandler) {
if (innerHandler is HttpClientHandler hch)
config(hch);
else
throw new Exception("ConfigureInnerHandler can only be used when IFlurlClientFactory.CreateInnerHandler returns an instance of HttpClientFactory.");
}
var outerHandler = _handlerBuilder.Build(_factory);

HttpMessageHandler outerHandler = innerHandler;
foreach (var createMW in Enumerable.Reverse(_addMiddleware)) {
var middleware = createMW();
foreach (var middleware in Enumerable.Reverse(_addMiddleware).Select(create => create())) {
middleware.InnerHandler = outerHandler;
outerHandler = middleware;
}
Expand All @@ -107,11 +111,27 @@ public IFlurlClient Build() {
config(httpCli);

var flurlCli = new FlurlClient(httpCli, _baseUrl);
foreach (var config in _configSettings) {
foreach (var config in _configSettings)
config(flurlCli.Settings);
}

return flurlCli;
}

// helper class to keep those compiler switches from getting too messy
private class HandlerBuilder<T> where T : HttpMessageHandler
{
public List<Action<T>> Configs { get; } = new();

public HttpMessageHandler Build(IFlurlClientFactory fac) {
var handler = fac.CreateInnerHandler();
foreach (var config in Configs) {
if (handler is T h)
config(h);
else
throw new Exception($"ConfigureInnerHandler expected an instance of {typeof(T).Name} but received an instance of {handler.GetType().Name}.");
}
return handler;
}
}
}
}
60 changes: 29 additions & 31 deletions src/Flurl.Http/Configuration/FlurlClientFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,23 @@
namespace Flurl.Http.Configuration
{
/// <summary>
/// Interface for methods used to build and cache IFlurlClient instances.
/// Interface for helper methods used to construct IFlurlClient instances.
/// </summary>
public interface IFlurlClientFactory
{
/// <summary>
/// Defines how HttpClient should be instantiated and configured by default. Do NOT attempt
/// to cache/reuse HttpClient instances here - that should be done at the FlurlClient level
/// via a custom FlurlClientFactory that gets registered globally.
/// Creates and configures a new HttpClient as needed when a new IFlurlClient instance is created.
/// Implementors should NOT attempt to cache or reuse HttpClient instances here - their lifetime is
/// bound one-to-one with an IFlurlClient, whose caching and reuse is managed by IFlurlClientCache.
/// </summary>
/// <param name="handler">The HttpMessageHandler used to construct the HttpClient.</param>
/// <returns></returns>
/// <param name="handler">The HttpMessageHandler passed to the constructor of the HttpClient.</param>
HttpClient CreateHttpClient(HttpMessageHandler handler);

/// <summary>
/// Defines how the HttpMessageHandler used by HttpClients that are created by
/// this factory should be instantiated and configured.
/// Creates and configures a new HttpMessageHandler as needed when a new IFlurlClient instance is created.
/// The default implementation creates an instance of SocketsHttpHandler for platforms that support it,
/// otherwise HttpClientHandler.
/// </summary>
/// <returns></returns>
HttpMessageHandler CreateInnerHandler();
}

Expand All @@ -42,38 +41,37 @@ public static class FlurlClientFactoryExtensions
/// </summary>
public class DefaultFlurlClientFactory : IFlurlClientFactory
{
/// <summary>
/// Override in custom factory to customize the creation of HttpClient used in all Flurl HTTP calls
/// (except when one is passed explicitly to the FlurlClient constructor). In order not to lose
/// Flurl.Http functionality, it is recommended to call base.CreateClient and customize the result.
/// </summary>
/// <inheritdoc />
public virtual HttpClient CreateHttpClient(HttpMessageHandler handler) {
return new HttpClient(handler);
}

/// <summary>
/// Override in custom factory to customize the creation of the top-level HttpMessageHandler used in all
/// Flurl HTTP calls (except when an HttpClient is passed explicitly to the FlurlClient constructor).
/// In order not to lose Flurl.Http functionality, it is recommended to call base.CreateMessageHandler, and
/// either customize the returned HttpClientHandler, or set it as the InnerHandler of a DelegatingHandler.
/// Creates and configures a new HttpMessageHandler as needed when a new IFlurlClient instance is created.
/// </summary>
public virtual HttpMessageHandler CreateInnerHandler() {
var httpClientHandler = new HttpClientHandler();
// Flurl has its own mechanisms for managing cookies and redirects, so we need to disable them in the inner handler.
#if NETCOREAPP2_1_OR_GREATER
var handler = new SocketsHttpHandler {
UseCookies = false,
AllowAutoRedirect = false,
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
};
#else
var handler = new HttpClientHandler();

// flurl has its own mechanisms for managing cookies and redirects
if (handler.SupportsRedirectConfiguration)
handler.AllowAutoRedirect = false;

try { httpClientHandler.UseCookies = false; }
catch (PlatformNotSupportedException) { } // look out for WASM platforms (#543)
// #266
// deflate not working? see #474
if (handler.SupportsAutomaticDecompression)
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;

if (httpClientHandler.SupportsRedirectConfiguration)
httpClientHandler.AllowAutoRedirect = false;

if (httpClientHandler.SupportsAutomaticDecompression) {
// #266
// deflate not working? see #474
httpClientHandler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
}
return httpClientHandler;
try { handler.UseCookies = false; }
catch (PlatformNotSupportedException) { } // look out for WASM platforms (#543)
#endif
return handler;
}
}
}
17 changes: 15 additions & 2 deletions test/Flurl.Test/Http/FlurlClientBuilderTests.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Flurl.Http;
Expand Down Expand Up @@ -52,6 +50,21 @@ public async Task can_add_middleware() {
Assert.AreEqual("blocked by flurl!", resp);
}

[Test]
public void inner_hanlder_is_SocketsHttpHandler_when_supported() {
var supported = typeof(HttpClientHandler).Assembly.DefinedTypes.Any(t => t.Name == "SocketsHttpHandler");
#if NET
Assert.IsTrue(supported, "SocketsHttpHandler should be defined"); // sanity check (tests the test, basically)
#endif
HttpMessageHandler handler = null;
new FlurlClientBuilder()
.ConfigureInnerHandler(h => handler = h)
.Build();

var expected = supported ? "SocketsHttpHandler" : "HttpClientHandler";
Assert.AreEqual(expected, handler?.GetType().Name);
}

class BlockingHandler : DelegatingHandler
{
private readonly string _msg;
Expand Down

0 comments on commit d5ff66e

Please sign in to comment.