Skip to content

Commit

Permalink
Make ProxyHttpClientFactory public and extensible #805 #843
Browse files Browse the repository at this point in the history
  • Loading branch information
Tratcher committed Apr 2, 2021
1 parent 28f748a commit c842d03
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 50 deletions.
53 changes: 18 additions & 35 deletions docs/docfx/articles/proxyhttpclientconfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,10 +235,22 @@ public void ConfigureServices(IServiceCollection services)
}
```

## Configuring the http client

`ConfigureClient` provides a callback to customize the `SocketsHttpHandler` settings used for proxying requests. This will be called each time a cluster is added or changed. Cluster settings are applied to the handler before the callback. Custom data can be provided in the cluster metadata.

```C#
services.AddReverseProxy()
.ConfigureClient((context, handler) =>
{
handler.Expect100ContinueTimeout = TimeSpan.FromMilliseconds(300);
})
```

## Custom IProxyHttpClientFactory
In case the direct control on a proxy HTTP client construction is necessary, the default [IProxyHttpClientFactory](xref:Yarp.ReverseProxy.Service.Proxy.Infrastructure.IProxyHttpClientFactory) can be replaced with a custom one. In example, that custom logic can use [Cluster](xref:Yarp.ReverseProxy.Abstractions.Cluster)'s metadata as an extra data source for [HttpMessageInvoker](https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpmessageinvoker?view=netcore-3.1) configuration. However, it's still recommended for any custom factory to set the following `HttpMessageInvoker` properties to the same values as the default factory does in order to preserve a correct reverse proxy behavior.
In case the direct control on a proxy HTTP client construction is necessary, the default [IProxyHttpClientFactory](xref:Yarp.ReverseProxy.Service.Proxy.Infrastructure.IProxyHttpClientFactory) can be replaced with a custom one. For some customizations you can derive from the default [ProxyHttpClientFactory](xref:Yarp.ReverseProxy.Service.Proxy.Infrastructure.ProxyHttpClientFactory) and override the methods that configure the client.

Always return an HttpMessageInvoker instance rather than an HttpClient instance which derives from HttpMessageInvoker. HttpClient buffers responses by default which breaks streaming scenarios and increases memory usage and latency.
It's recommended that any custom factory set the following `SocketsHttpHandler` properties to the same values as the default factory does in order to preserve a correct reverse proxy behavior and avoid unnecessary overhead.

```C#
new SocketsHttpHandler
Expand All @@ -250,20 +262,17 @@ new SocketsHttpHandler
};
```

Always return an HttpMessageInvoker instance rather than an HttpClient instance which derives from HttpMessageInvoker. HttpClient buffers responses by default which breaks streaming scenarios and increases memory usage and latency.

Custom data can be provided in the cluster metadata.

The below is an example of a custom `IProxyHttpClientFactory` implementation.

```C#
public class CustomProxyHttpClientFactory : IProxyHttpClientFactory
{
public HttpMessageInvoker CreateClient(ProxyHttpClientContext context)
{
if (context.OldClient != null && context.NewOptions == context.OldOptions)
{
return context.OldClient;
}

var newClientOptions = context.NewOptions;

var handler = new SocketsHttpHandler
{
UseProxy = false,
Expand All @@ -272,32 +281,6 @@ public class CustomProxyHttpClientFactory : IProxyHttpClientFactory
UseCookies = false
};

if (newClientOptions.SslProtocols.HasValue)
{
handler.SslOptions.EnabledSslProtocols = newClientOptions.SslProtocols.Value;
}
if (newClientOptions.ClientCertificate != null)
{
handler.SslOptions.ClientCertificates = new X509CertificateCollection
{
newClientOptions.ClientCertificate
};
}
if (newClientOptions.MaxConnectionsPerServer != null)
{
handler.MaxConnectionsPerServer = newClientOptions.MaxConnectionsPerServer.Value;
}
if (newClientOptions.DangerousAcceptAnyServerCertificate)
{
handler.SslOptions.RemoteCertificateValidationCallback = (sender, cert, chain, errors) => cert.Subject == "dev.mydomain";
}
#if NET
if (newClientOptions.RequestHeaderEncoding != null)
{
handler.RequestHeaderEncodingSelector = (_, _) => newClientOptions.RequestHeaderEncoding;
}
#endif

return new HttpMessageInvoker(handler, disposeHandler: true);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System;
using System.Net.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
Expand All @@ -12,6 +13,7 @@
using Yarp.ReverseProxy.Service;
using Yarp.ReverseProxy.Service.Config;
using Yarp.ReverseProxy.Service.Proxy;
using Yarp.ReverseProxy.Service.Proxy.Infrastructure;
using Yarp.ReverseProxy.Utilities;

namespace Microsoft.Extensions.DependencyInjection
Expand Down Expand Up @@ -121,5 +123,20 @@ public static IReverseProxyBuilder AddTransformFactory<T>(this IReverseProxyBuil
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ITransformFactory, T>());
return builder;
}

/// <summary>
/// Provides a callback to customize <see cref="SocketsHttpHandler"/> settings used for proxying requests.
/// This will be called each time a cluster is added or changed. Cluster settings are applied to the handler before
/// the callback. Custom data can be provided in the cluster metadata.
/// </summary>
public static IReverseProxyBuilder ConfigureClient(this IReverseProxyBuilder builder, Action<ProxyHttpClientContext, SocketsHttpHandler> configure)
{
builder.Services.AddSingleton<IProxyHttpClientFactory>(services =>
{
var logger = services.GetRequiredService<ILogger<ProxyHttpClientFactory>>();
return new CallbackProxyHttpClientFactory(logger, configure);
});
return builder;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Net.Http;
using Microsoft.Extensions.Logging;

namespace Yarp.ReverseProxy.Service.Proxy.Infrastructure
{
internal class CallbackProxyHttpClientFactory : ProxyHttpClientFactory
{
private readonly Action<ProxyHttpClientContext, SocketsHttpHandler> _configureClient;

internal CallbackProxyHttpClientFactory(ILogger<ProxyHttpClientFactory> logger,
Action<ProxyHttpClientContext, SocketsHttpHandler> configureClient) : base(logger)
{
_configureClient = configureClient ?? throw new ArgumentNullException(nameof(configureClient));
}

protected override void ConfigureHandler(ProxyHttpClientContext context, SocketsHttpHandler handler)
{
base.ConfigureHandler(context, handler);
_configureClient(context, handler);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Net.Http;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Yarp.ReverseProxy.Abstractions;
using Yarp.ReverseProxy.Telemetry;

Expand All @@ -14,10 +15,15 @@ namespace Yarp.ReverseProxy.Service.Proxy.Infrastructure
/// <summary>
/// Default implementation of <see cref="IProxyHttpClientFactory"/>.
/// </summary>
internal class ProxyHttpClientFactory : IProxyHttpClientFactory
public class ProxyHttpClientFactory : IProxyHttpClientFactory
{
private readonly ILogger<ProxyHttpClientFactory> _logger;

/// <summary>
/// Initializes a new instance of the <see cref="ProxyHttpClientFactory"/> class.
/// </summary>
public ProxyHttpClientFactory() : this(NullLogger<ProxyHttpClientFactory>.Instance) { }

/// <summary>
/// Initializes a new instance of the <see cref="ProxyHttpClientFactory"/> class.
/// </summary>
Expand All @@ -35,7 +41,6 @@ public HttpMessageInvoker CreateClient(ProxyHttpClientContext context)
return context.OldClient;
}

var newClientOptions = context.NewOptions;
var handler = new SocketsHttpHandler
{
UseProxy = false,
Expand All @@ -46,6 +51,34 @@ public HttpMessageInvoker CreateClient(ProxyHttpClientContext context)
// NOTE: MaxResponseHeadersLength = 64, which means up to 64 KB of headers are allowed by default as of .NET Core 3.1.
};

ConfigureHandler(context, handler);

var middleware = WrapHandler(context, handler);

Log.ProxyClientCreated(_logger, context.ClusterId);

return new HttpMessageInvoker(middleware, disposeHandler: true);
}

/// <summary>
/// Checks if the options have changed since the old client was created. If not then the
/// old client will be re-used. Re-use can avoid the latency of creating new connections.
/// </summary>
protected virtual bool CanReuseOldClient(ProxyHttpClientContext context)
{
return context.OldClient != null && context.NewOptions == context.OldOptions;
}

/// <summary>
/// Allows configuring the <see cref="SocketsHttpHandler"/> instance. The base implementation
/// applies settings from <see cref="ProxyHttpClientContext.NewOptions"/>.
/// <see cref="SocketsHttpHandler.UseProxy"/>, <see cref="SocketsHttpHandler.AllowAutoRedirect"/>,
/// <see cref="SocketsHttpHandler.AutomaticDecompression"/>, and <see cref="SocketsHttpHandler.UseCookies"/>
/// are disabled prior to this call.
/// </summary>
protected virtual void ConfigureHandler(ProxyHttpClientContext context, SocketsHttpHandler handler)
{
var newClientOptions = context.NewOptions;
if (newClientOptions.SslProtocols.HasValue)
{
handler.SslOptions.EnabledSslProtocols = newClientOptions.SslProtocols.Value;
Expand Down Expand Up @@ -73,23 +106,12 @@ public HttpMessageInvoker CreateClient(ProxyHttpClientContext context)
handler.RequestHeaderEncodingSelector = (_, _) => newClientOptions.RequestHeaderEncoding;
}
#endif

var webProxy = TryCreateWebProxy(newClientOptions.WebProxy);
if (webProxy != null)
{
handler.Proxy = webProxy;
handler.UseProxy = true;
}

Log.ProxyClientCreated(_logger, context.ClusterId);

var activityContextHeaders = newClientOptions.ActivityContextHeaders.GetValueOrDefault(ActivityContextHeaders.BaggageAndCorrelationContext);
if (activityContextHeaders != ActivityContextHeaders.None)
{
return new HttpMessageInvoker(new ActivityPropagationHandler(activityContextHeaders, handler), disposeHandler: true);
}

return new HttpMessageInvoker(handler, disposeHandler: true);
}

private static IWebProxy TryCreateWebProxy(WebProxyOptions webProxyOptions)
Expand All @@ -107,9 +129,19 @@ private static IWebProxy TryCreateWebProxy(WebProxyOptions webProxyOptions)
return webProxy;
}

private static bool CanReuseOldClient(ProxyHttpClientContext context)
/// <summary>
/// Adds any wrapping middleware around the <see cref="HttpMessageHandler"/>.
/// The base implementation conditionally includes the <see cref="ActivityPropagationHandler"/>.
/// </summary>
protected virtual HttpMessageHandler WrapHandler(ProxyHttpClientContext context, HttpMessageHandler handler)
{
return context.OldClient != null && context.NewOptions == context.OldOptions;
var activityContextHeaders = context.NewOptions.ActivityContextHeaders.GetValueOrDefault(ActivityContextHeaders.BaggageAndCorrelationContext);
if (activityContextHeaders != ActivityContextHeaders.None)
{
handler = new ActivityPropagationHandler(activityContextHeaders, handler);
}

return handler;
}

private static class Log
Expand Down
4 changes: 4 additions & 0 deletions testassets/ReverseProxy.Code/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ public void ConfigureServices(IServiceCollection services)

services.AddReverseProxy()
.LoadFromMemory(routes, clusters)
.ConfigureClient((context, handler) =>
{
handler.Expect100ContinueTimeout = TimeSpan.FromMilliseconds(300);
})
.AddTransformFactory<MyTransformFactory>()
.AddTransforms<MyTransformProvider>()
.AddTransforms(transformBuilderContext =>
Expand Down

0 comments on commit c842d03

Please sign in to comment.