Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make ProxyHttpClientFactory public and extensible #869

Merged
merged 1 commit into from
Apr 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) { }
Tratcher marked this conversation as resolved.
Show resolved Hide resolved

/// <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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this HttpMessageInvoker ever get disposed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, there's no way of telling when it's no longer in use, except that it will get GC'd.

}

/// <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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expect100ContinueTimeout or ConfigureClient?

  • ConfigureClient this lets users change basic settings without implementing a new class.
  • Expect100ContinueTimeout is only used here has an example.

})
.AddTransformFactory<MyTransformFactory>()
.AddTransforms<MyTransformProvider>()
.AddTransforms(transformBuilderContext =>
Expand Down