Skip to content

Commit

Permalink
redo of #769 to address #772
Browse files Browse the repository at this point in the history
  • Loading branch information
Todd committed Oct 27, 2023
1 parent 8dbd2b9 commit 32df3c3
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 61 deletions.
91 changes: 46 additions & 45 deletions src/Flurl.Http/Configuration/FlurlClientBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Runtime.Versioning;

namespace Flurl.Http.Configuration
{
Expand All @@ -21,12 +22,19 @@ public interface IFlurlClientBuilder
IFlurlClientBuilder ConfigureHttpClient(Action<HttpClient> configure);

/// <summary>
/// Configure the inner-most HttpMessageHandler associated with this IFlurlClient.
/// Configure the inner-most HttpMessageHandler (an instance of HttpClientHandler) associated with this IFlurlClient.
/// </summary>
#if NETCOREAPP2_1_OR_GREATER
IFlurlClientBuilder ConfigureInnerHandler(Action<SocketsHttpHandler> configure);
#else
IFlurlClientBuilder ConfigureInnerHandler(Action<HttpClientHandler> configure);

#if NET
/// <summary>
/// Configure a SocketsHttpHandler instead of HttpClientHandler as the inner-most HttpMessageHandler.
/// Note that HttpClientHandler has broader platform support and defers its work to SocketsHttpHandler
/// on supported platforms. It is recommended to explicitly use SocketsHttpHandler ONLY if you
/// need to directly configure its properties that aren't available on HttpClientHandler.
/// </summary>
[UnsupportedOSPlatform("browser")]
IFlurlClientBuilder UseSocketsHttpHandler(Action<SocketsHttpHandler> configure);
#endif

/// <summary>
Expand All @@ -45,33 +53,24 @@ public interface IFlurlClientBuilder
/// </summary>
public class FlurlClientBuilder : IFlurlClientBuilder
{
private readonly IFlurlClientFactory _factory;
private IFlurlClientFactory _factory = new DefaultFlurlClientFactory();

private readonly string _baseUrl;
private readonly List<Func<DelegatingHandler>> _addMiddleware = 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
private readonly List<Action<FlurlHttpSettings>> _settingsConfigs = new();
private readonly List<Action<HttpClient>> _clientConfigs = new();
private readonly List<Action<HttpMessageHandler>> _handlerConfigs = new();

/// <summary>
/// Creates a new FlurlClientBuilder.
/// </summary>
public FlurlClientBuilder(string baseUrl = null) : this(new DefaultFlurlClientFactory(), baseUrl) { }

/// <summary>
/// Creates a new FlurlClientBuilder.
/// </summary>
internal FlurlClientBuilder(IFlurlClientFactory factory, string baseUrl) {
_factory = factory;
public FlurlClientBuilder(string baseUrl = null) {
_baseUrl = baseUrl;
}

/// <inheritdoc />
public IFlurlClientBuilder WithSettings(Action<FlurlHttpSettings> configure) {
_configSettings.Add(configure);
_settingsConfigs.Add(configure);
return this;
}

Expand All @@ -83,55 +82,57 @@ public IFlurlClientBuilder AddMiddleware(Func<DelegatingHandler> create) {

/// <inheritdoc />
public IFlurlClientBuilder ConfigureHttpClient(Action<HttpClient> configure) {
_configClient.Add(configure);
_clientConfigs.Add(configure);
return this;
}

/// <inheritdoc />
#if NETCOREAPP2_1_OR_GREATER
public IFlurlClientBuilder ConfigureInnerHandler(Action<SocketsHttpHandler> configure) {
#else
public IFlurlClientBuilder ConfigureInnerHandler(Action<HttpClientHandler> configure) {
#if NET
if (_factory is SocketsHandlerFlurlClientFactory && _handlerConfigs.Any())
throw new FlurlConfigurationException("ConfigureInnerHandler and UseSocketsHttpHandler cannot be used together. The former configures and instance of HttpClientHandler and would be ignored when switching to SocketsHttpHandler.");
#endif
_handlerBuilder.Configs.Add(configure);
_handlerConfigs.Add(h => configure(h as HttpClientHandler));
return this;
}

#if NET
/// <inheritdoc />
public IFlurlClientBuilder UseSocketsHttpHandler(Action<SocketsHttpHandler> configure) {
if (!SocketsHttpHandler.IsSupported)
throw new PlatformNotSupportedException("SocketsHttpHandler is not supported on one or more target platforms.");

if (_factory is DefaultFlurlClientFactory && _handlerConfigs.Any())
throw new FlurlConfigurationException("ConfigureInnerHandler and UseSocketsHttpHandler cannot be used together. The former configures and instance of HttpClientHandler and would be ignored when switching to SocketsHttpHandler.");

if (!(_factory is SocketsHandlerFlurlClientFactory))
_factory = new SocketsHandlerFlurlClientFactory();

_handlerConfigs.Add(h => configure(h as SocketsHttpHandler));
return this;
}
#endif

/// <inheritdoc />
public IFlurlClient Build() {
var outerHandler = _handlerBuilder.Build(_factory);
var outerHandler = _factory.CreateInnerHandler();
foreach (var config in _handlerConfigs)
config(outerHandler);

foreach (var middleware in Enumerable.Reverse(_addMiddleware).Select(create => create())) {
middleware.InnerHandler = outerHandler;
outerHandler = middleware;
}

var httpCli = _factory.CreateHttpClient(outerHandler);
foreach (var config in _configClient)
foreach (var config in _clientConfigs)
config(httpCli);

var flurlCli = new FlurlClient(httpCli, _baseUrl);
foreach (var config in _configSettings)
foreach (var config in _settingsConfigs)
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;
}
}
}
}
5 changes: 2 additions & 3 deletions src/Flurl.Http/Configuration/FlurlClientCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,14 @@ public interface IFlurlClientCache
/// </summary>
public class FlurlClientCache : IFlurlClientCache {
private readonly ConcurrentDictionary<string, Lazy<IFlurlClient>> _clients = new();
private readonly IFlurlClientFactory _factory = new DefaultFlurlClientFactory();
private Action<IFlurlClientBuilder> _configureAll;

/// <inheritdoc />
public IFlurlClientBuilder Add(string name, string baseUrl = null) {
if (_clients.ContainsKey(name))
throw new ArgumentException($"A client named '{name}' was already registered with this factory. AddClient should be called just once per client at startup.");

var builder = new FlurlClientBuilder(_factory, baseUrl);
var builder = new FlurlClientBuilder(baseUrl);
_clients[name] = CreateLazyInstance(builder);
return builder;
}
Expand All @@ -63,7 +62,7 @@ public virtual IFlurlClient Get(string name) {
if (name == null)
throw new ArgumentNullException(nameof(name));

Lazy<IFlurlClient> Create() => CreateLazyInstance(new FlurlClientBuilder(_factory, null));
Lazy<IFlurlClient> Create() => CreateLazyInstance(new FlurlClientBuilder());
return _clients.AddOrUpdate(name, _ => Create(), (_, existing) => existing.Value.IsDisposed ? Create() : existing).Value;
}

Expand Down
27 changes: 19 additions & 8 deletions src/Flurl.Http/Configuration/FlurlClientFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,6 @@ public virtual HttpClient CreateHttpClient(HttpMessageHandler handler) {
/// </summary>
public virtual HttpMessageHandler CreateInnerHandler() {
// 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();

if (handler.SupportsRedirectConfiguration)
Expand All @@ -70,8 +63,26 @@ public virtual HttpMessageHandler CreateInnerHandler() {

try { handler.UseCookies = false; }
catch (PlatformNotSupportedException) { } // look out for WASM platforms (#543)
#endif

return handler;
}
}

#if NET
/// <summary>
/// An implementation of IFlurlClientFactory that uses SocketsHttpHandler on supported platforms.
/// </summary>
public class SocketsHandlerFlurlClientFactory : DefaultFlurlClientFactory
{
/// <summary>
/// Creates and configures a new SocketsHttpHandler as needed when a new IFlurlClient instance is created.
/// </summary>
public override HttpMessageHandler CreateInnerHandler() => new SocketsHttpHandler {
// Flurl has its own mechanisms for managing cookies and redirects, so we need to disable them in the inner handler.
UseCookies = false,
AllowAutoRedirect = false,
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
};
}
#endif
}
5 changes: 4 additions & 1 deletion src/Flurl.Http/FlurlHttp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ public static class FlurlHttp
/// <summary>
/// Gets or creates the IFlurlClient that would be selected for sending the given IFlurlRequest when the clientless pattern is used.
/// </summary>
public static IFlurlClient GetClientForRequest(IFlurlRequest req) => Clients.Get(_cachingStrategy(req));
public static IFlurlClient GetClientForRequest(IFlurlRequest req) {
throw new Exception("here");
//return Clients.Get(_cachingStrategy(req));
}

/// <summary>
/// Sets a global caching strategy for getting or creating an IFlurlClient instance when the clientless pattern is used, e.g. url.GetAsync.
Expand Down
11 changes: 11 additions & 0 deletions src/Flurl.Http/FlurlHttpException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,15 @@ private static string BuildMessage(FlurlCall call, string expectedFormat) {
return msg + ((call == null) ? "." : $": {call}");
}
}

/// <summary>
/// An exception that is thrown when Flurl.Http has been misconfigured.
/// </summary>
public class FlurlConfigurationException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="FlurlConfigurationException"/> class.
/// </summary>
public FlurlConfigurationException(string message) : base(message) { }
}
}
27 changes: 23 additions & 4 deletions test/Flurl.Test/Http/FlurlClientBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,37 @@ public async Task can_add_middleware() {
}

[Test]
public void inner_hanlder_is_SocketsHttpHandler_when_supported() {
public void uses_HttpClientHandler_by_default() {
HttpMessageHandler handler = null;
new FlurlClientBuilder()
.ConfigureInnerHandler(h => handler = h)
.Build();
Assert.IsInstanceOf<HttpClientHandler>(handler);
}

#if NET
[Test]
public void can_use_SocketsHttpHandler() {
HttpMessageHandler handler = null;
new FlurlClientBuilder()
.UseSocketsHttpHandler(h => handler = h)
.Build();
Assert.IsInstanceOf<SocketsHttpHandler>(handler);
#else
Assert.IsInstanceOf<HttpClientHandler>(handler);
#endif
}

[Test]
public void cannot_mix_handler_types() {
Assert.Throws<FlurlConfigurationException>(() => new FlurlClientBuilder()
.ConfigureInnerHandler(_ => { })
.UseSocketsHttpHandler(_ => { }));

// reverse
Assert.Throws<FlurlConfigurationException>(() => new FlurlClientBuilder()
.UseSocketsHttpHandler(_ => { })
.ConfigureInnerHandler(_ => { }));
}
#endif

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

0 comments on commit 32df3c3

Please sign in to comment.