常见IHttpClientFactory
使用问题
在本文中,您将学习使用时可能遇到的一些最常见的问题IHttpClientFactory
创建HttpClient
实例。
IHttpClientFactory
是一种方便的方式来设置多个HttpClient
在 DI 容器中配置,配置日志记录,建立弹性策略等。 IHttpClientFactory
还封装了生存期管理和HttpClient
HttpMessageHandler
实例,以防止出现套接字耗尽和 DNS 更改丢失等问题。 有关如何在您的 .NET 应用IHttpClientFactory
程序中使用的概述,请参阅 .NET 中的 IHttpClientFactory
由于与依赖注入 DI 集成的复杂性IHttpClientFactory
,您可能会遇到一些难以发现和排除的问题。 本文中列出的场景还包含建议,您可以主动应用这些建议以避免潜在问题。
HttpClient
不尊重Scoped
生命周期
如果您需要访问任何作用域服务,例如,您可能会遇到问题HttpMessageHandler
,或从内部访问某些作用域缓存HttpContext
那里保存的数据可能会“消失”,或者相反,在不应该的情况下“持久化”。 这是由依赖注入(DI)引起的范围不匹配在应用程序上下文和处理程序实例之间,这是一个已知的限制IHttpClientFactory
。
IHttpClientFactory
为每个 HttpMessageHandler
实例创建单独的 DI 范围。 这些处理程序范围与应用程序上下文范围是不同的(例如,ASP.NET Core 的传入请求范围或用户创建的手动 DI 范围),因此它们不会共享作用域服务实例。
由于这一限制:
- 在作用域服务中“外部”缓存的任何数据在内部将不可用
HttpMessageHandler
。 - 在内部缓存的任何数据或其作用域依赖项
HttpMessageHandler
可以从多个应用程序 DI 范围(例如,来自不同的传入请求)中观察到,因为它们可以共享同一个处理程序。
考虑以下建议,以帮助缓解这一已知限制:
❌请勿在实例或其依赖项内部缓存任何与范围相关的信息HttpContext
(例如来自)HttpMessageHandler
以避免泄露敏感信息。
❌请勿使用 cookie,因为CookieContainer
它们将与处理程序一起共享。
✔️ 考虑不存储该信息,或仅在实例HttpRequestMessage
内传递它。
要在处理程序旁边传递任意信息HttpRequestMessage
,您可以使用HttpRequestMessage.Options属性。
考虑将所有与范围相关的(例如,身份验证)逻辑封DelegatingHandler
装在一个不由发送处理程序创IHttpClientFactory
建的单独组件中,并使用IHttpClientFactory
它来包装由发送处理程序创建的处理程序。
要创建一个HttpMessageHandler
没有HttpClient
任何上下IHttpMessageHandlerFactory.CreateHandler文的实例,请调用任何注册的命名客户端。 在该案例中,您将需要使用HttpClient
组合处理程序自己创建一个实例。 您可以在 GitHub 上找到此解决方法的完整可运行示例。
有关更多信息,请参阅指南中的 IHttpClientFactory 中的消息处理程序范围IHttpClientFactory
部分。
HttpClient
不尊重 DNS 更改
即使使用了 DNS,IHttpClientFactory
仍然可能遇到过时的 DNS 问题。 如果一个HttpClient
实例被捕获在服务中,或者一般Singleton
来说,被存储在某个地方的时间超过了指定的时间,这种情况通常会发生HandlerLifetime
。 HttpClient
如果相应的类型化客户端被单例捕获,它也会被捕获。
❌请不要长HttpClient
时间IHttpClientFactory
缓存实例。
❌请勿将类型化客户Singleton
端实例注入到服务中。
✔️ 考虑及时或每次需要时IHttpClientFactory
从请求一个客户端。 由工厂创建的客户端是安全可以处理的。
HttpClient
创建的 IHttpClientFactory
实例是短期的。
回收和重新创建
HttpMessageHandler
在其生存期到期时对于IHttpClientFactory
至关重要,可确保处理程序对 DNS 更改做出反应。HttpClient
在创建特定处理程序实例时与之绑定,因此应及时请求新的HttpClient
实例,以确保客户端将获取更新后的处理程序。处理由工
HttpClient
厂创建的此类实例不会导致套接字耗尽,因为其处理不会触发的处理HttpMessageHandler
。IHttpClientFactory
跟踪和释放用于创建HttpClient
实例的资源,特别是HttpMessageHandler
实例,只要其生存期到期,并且HttpClient
不再使用这些实例。
类型化客户端旨在短生命周期,因为实例HttpClient
被注入到构造函数中,因此它将共享类型化客户端的生命周期。
有关更多信息,请参阅指南中的HttpClient
生命周期管理和避免在单例服务中使用类型化客户端IHttpClientFactory
部分。
HttpClient
使用了过多的套接字。
即使使用了IHttpClientFactory
套接字,仍然可能在特定使用场景中遇到套接字耗尽的问题。 默认情况下,HttpClient
未限制并发请求的数量。 如果同时启动大量 HTTP/1.1 请求,每个请求都会触发一个新的 HTTP 连接尝试,因为连接池中没有可用连接且没有设置限制。
❌ 请勿在未指定限制的情况下同时启动大量 HTTP/1.1 请求。
✔️ 考虑将(或者如果HttpClientHandler.MaxConnectionsPerServer您将其用作主SocketsHttpHandler.MaxConnectionsPerServer处理程序)设置为合理的值。 请注意,这些限制仅适用于特定的处理程序实例。
✔️ 考虑使用 HTTP/2,它允许通过单个 TCP 连接多路复用请求。
类型化客户端HttpClient
注入错误
在某些情况下,HttpClient
可能会意外地注入到类型化客户端中。 大多数情况下,根本原因在于错误的配置,因为根据依赖注入的设计,对服务的任何后续注册都会覆盖先前的注册。
类型化客户端 使用 命名客户端 “在后台”:添加 类型化客户端 隐式注册并将其链接到 命名客户端。 客户端名称,除非明确提供,否则将设置为的类型名称TClient
。 如果使用AddHttpClient<TClient,TImplementation>
重载,则这是对中的第一个TClient,TImplementation
重载。
因此,注册一个类型化客户端会执行两个独立的操作:
- 注册命名客户端(在简单的默认情况下,名称为
typeof(TClient).Name
)。 Transient
使用TClient
或TClient,TImplementation
提供注册服务。
以下两个语句在技术上是相同的:
services.AddHttpClient<ExampleClient>(c => c.BaseAddress = new Uri("http://example.com"));
// -OR-
services.AddHttpClient(nameof(ExampleClient), c => c.BaseAddress = new Uri("http://example.com")) // register named client
.AddTypedClient<ExampleClient>(); // link the named client to a typed client
在一个简单的案例中,它也将类似于以下内容:
services.AddHttpClient(nameof(ExampleClient), c => c.BaseAddress = new Uri("http://example.com")); // register named client
// register plain Transient service and link it to the named client
services.AddTransient<ExampleClient>(s =>
new ExampleClient(
s.GetRequiredService<IHttpClientFactory>().CreateClient(nameof(ExampleClient))));
请考虑以下示例,了解如何断开类型化客户端和命名客户端之间的链接。
类型化客户端被第二次注册
❌ 请勿单独注册 类型化客户端 — 调用已自动注册该 AddHttpClient<T>
客户端。
如果类型化客户端第二次错误地注册为普通暂时性服务,则会覆盖该客户端HttpClientFactory
添加的注册,从而中断指向命名客户端的链接。 它将像丢失配置一样 HttpClient
,因为未配置的 HttpClient
配置将改为注入 到类型化的客户端 中。
可能会令人困惑的是,使用了一个“错误”的,HttpClient
而不是抛出异常。 发生这种情况是因为 “default” 未配置HttpClient
(名称Options.DefaultName 为(string.Empty
)的客户端注册为普通暂时性服务,以启用最基本的HttpClientFactory
使用方案。 这就是链接断开且类型化客户端仅成为普通服务的原因,此“默认”HttpClient
自然会注入到相应的构造函数参数中。
不同类型客户端在通用接口上注册
如果两个不同的类型客户端在公共接口上注册,它们将重复使用同一命名的客户端。 这似乎是第一个类型化的客户端,第二个命名客户端“错误”注入。
❌ 请勿在单个接口上注册多个类型化客户端,而无需显式指定名称。
✔️ 请考虑单独注册和配置命名客户端,然后将其链接到一个或多个类型化客户端,方法是在调用中AddHttpClient<T>
指定名称或在命名客户端设置期间调用AddTypedClient
。
根据设计,使用相同名称多次注册和配置命名客户端只是将配置操作附加到现有操作的列表中。 这种行为HttpClientFactory
可能并不明显,但与选项模式和配置 API 使用Configure的方法相同。
这主要对于高级处理程序配置非常有用,例如,向外部定义的命名客户端添加自定义处理程序,HttpClient
或为测试模拟主处理程序,但它也适用于实例配置。 例如,以下示例中的三个HttpClient
示例将采用相同的方式进行配置(同时BaseAddress
设置和DefaultRequestHeaders
设置):
// one configuration callback
services.AddHttpClient("example", c =>
{
c.BaseAddress = new Uri("http://example.com");
c.DefaultRequestHeaders.UserAgent.ParseAdd("HttpClient/8.0");
});
// -OR-
// two configuration callbacks
services.AddHttpClient("example", c => c.BaseAddress = new Uri("http://example.com"))
.ConfigureHttpClient(c => c.DefaultRequestHeaders.UserAgent.ParseAdd("HttpClient/8.0"));
// -OR-
// two configuration callbacks in separate AddHttpClient calls
services.AddHttpClient("example", c => c.BaseAddress = new Uri("http://example.com"));
services.AddHttpClient("example")
.ConfigureHttpClient(c => c.DefaultRequestHeaders.UserAgent.ParseAdd("HttpClient/8.0"));
这允许将类型化客户端链接到已定义的命名客户端,并将多个类型化客户端链接到单个命名客户端。 当使用带参数的重载时,name
这一点更加明显:
services.AddHttpClient("LogClient", c => c.BaseAddress = new Uri(LogServerAddress));
services.AddHttpClient<FooLogger>("LogClient");
services.AddHttpClient<BarLogger>("LogClient");
在命名客户端配置期间调用AddTypedClient也可以实现相同的操作:
services.AddHttpClient("LogClient", c => c.BaseAddress = new Uri(LogServerAddress))
.AddTypedClient<FooLogger>()
.AddTypedClient<BarLogger>();
但是,如果不想重复使用同一命名客户端,但仍希望在同一接口上注册客户端,可以通过显式指定不同的名称来执行此操作:
services.AddHttpClient<ITypedClient, ExampleClient>(nameof(ExampleClient),
c => c.BaseAddress = new Uri("http://example.com"));
services.AddHttpClient<ITypedClient, GithubClient>(nameof(GithubClient),
c => c.BaseAddress = new Uri("https://github.com"));