Распространенные IHttpClientFactory вопросы использования

В этой статье вы узнаете о некоторых наиболее распространенных проблемах, с которыми можно столкнуться при использовании IHttpClientFactory для создания HttpClient экземпляров.

IHttpClientFactory — удобный способ настроить несколько HttpClient конфигураций в контейнере DI, настроить ведение журнала, настроить стратегии устойчивости и многое другое. IHttpClientFactory также инкапсулирует управление жизненным циклом экземпляров HttpClient и HttpMessageHandler, чтобы предотвратить такие проблемы, как исчерпание сокетов и потеря изменений DNS. Общие сведения об использовании IHttpClientFactory в приложении .NET см. в разделе IHttpClientFactory с .NET.

В связи с комплексной природой интеграции IHttpClientFactory с DI вы можете столкнуться с некоторыми проблемами, которые будет трудно выявить и устранить. В сценариях, перечисленных в этой статье, также содержатся рекомендации, которые можно применять заранее, чтобы избежать потенциальных проблем.

HttpClient не учитывает Scoped время жизни

Если вам потребуется получить доступ к любой службе с областью действия, например HttpContext или к какому-либо кэшу с областью действия, из HttpMessageHandler. Данные, сохраненные там, могут либо «исчезнуть», либо, наоборот, «сохраниться», когда этого не должно происходить. Это вызвано несоответствием области внедрения зависимостей (DI) между контекстом приложения и экземпляром обработчика, и это известное ограничение.IHttpClientFactory

IHttpClientFactory создает отдельную область DI для каждого HttpMessageHandler экземпляра. Эти области обработчика отличаются от областей контекста приложения (например, области входящих запросов в ASP.NET Core или созданной пользователем области DI), поэтому они не будут использовать совместные экземпляры служб с областью действия.

В результате этого ограничения:

  • Любые данные, кэшированные "внешне" в службе с ограниченной областью действия, не будут доступны в HttpMessageHandlerпределах.
  • Все данные, кэшированные внутри HttpMessageHandler или его зависимостей , могут наблюдаться из нескольких областей di приложения (например, из разных входящих запросов), так как они могут совместно использовать один и тот же обработчик.

Рассмотрим следующие рекомендации, которые помогут устранить это известное ограничение:

❌ НЕ кэшируйте любую информацию, связанную с областью действия (например, данные из HttpContext) в HttpMessageHandler экземплярах или их зависимостях, чтобы предотвратить утечку конфиденциальной информации.

❌ Не используйте файлы cookie, так как CookieContainer они будут совместно использоваться обработчиком.

✔️ Подумайте о том, чтобы не хранить информацию или передавать ее только в экземпляре HttpRequestMessage.

Для передачи произвольных сведений вместе с HttpRequestMessage можно использовать свойство HttpRequestMessage.Options.

✔️ Рассмотрите возможность инкапсулировать всю логику, связанную с областью (например, аутентификацию) в отдельном DelegatingHandler объекте, который не создан, IHttpClientFactoryи используйте его для упаковки созданного IHttpClientFactoryобработчика.

Чтобы создать только без , вызовите для любого зарегистрированного именованного клиента . В этом случае необходимо создать HttpClient экземпляр самостоятельно с помощью объединенного обработчика. Вы можете найти полный пример запуска для этого обходного решения на сайте GitHub.

Дополнительные сведения см. в разделе Области обработчика сообщений в IHttpClientFactory в IHttpClientFactory рекомендациях.

HttpClient не учитывает изменения 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 по управлению временем существования и избегайте типизированных клиентов в разделах служб singleton в IHttpClientFactory рекомендациях.

HttpClient использует слишком много сокетов

Даже при использовании IHttpClientFactory, все равно может возникнуть проблема с исчерпанием сокета при определенном сценарии использования. По умолчанию HttpClient не ограничивает количество одновременных запросов. Если одновременно запускается большое количество запросов HTTP/1.1, каждое из них в конечном итоге активирует новую попытку HTTP-подключения, так как в пуле нет свободного подключения и ограничения не задано.

❌ Не запускайте большое количество запросов HTTP/1.1 одновременно без указания ограничений.

✔️ Установите HttpClientHandler.MaxConnectionsPerServer (или SocketsHttpHandler.MaxConnectionsPerServer, если вы используете его в качестве основного обработчика) на разумное значение. Обратите внимание, что эти ограничения применяются только к конкретному экземпляру обработчика.

✔️ Рекомендуется использовать ПРОТОКОЛ HTTP/2, который позволяет мультиплексирование запросов через одно TCP-подключение.

Типизированному клиенту неправильно внедрили HttpClient

Могут возникнуть различные ситуации, в которых возможно неожиданное внедрение HttpClient в типизированного клиента. В большинстве случаев основная причина будет находиться в ошибочной конфигурации, так как, по проектированию DI, любая последующая регистрация службы переопределяет предыдущую.

Типизированные клиенты используют именованные клиенты "под капотом": добавление типизированного клиента неявно регистрирует и связывает его с именованным клиентом. Имя клиента, если явно не указано, будет установлено как имя типа TClient. Это будет первый из TClient,TImplementation пары, если используются AddHttpClient<TClient,TImplementation> перегрузки.

Таким образом, регистрация типизированного клиента выполняет две отдельные задачи:

  1. Регистрирует именованного клиента (в простейшем случае по умолчанию это имя typeof(TClient).Name).
  2. Регистрирует службу, используя 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> вызова.

Если типизированный клиент ошибочно регистрируется во второй раз в качестве обычной транзитной службы, это перезаписывает регистрацию, добавленную с помощью IHttpClientFactory, разрывая связь с именованным клиентом. Он будет проявляться так, словно HttpClient потеряла конфигурацию, так как вместо этого ненастроенная HttpClient будет внедрена в типизированный клиент.

Это может вызвать путаницу, что вместо того, чтобы вызвать исключение, используется «неверный» HttpClient. Это происходит потому, что "по умолчанию не настроенный" HttpClient — клиент с именем Options.DefaultName (string.Empty) зарегистрирован как простой сквозной сервис, чтобы обеспечить самый базовый сценарий использования IHttpClientFactory. Именно поэтому, после того как ссылка будет нарушена и типизированный клиент станет обычной службой, этот "по умолчанию" HttpClient естественным образом будет внедрен в соответствующий параметр конструктора.

Разные типизированные клиенты регистрируются в общем интерфейсе

Если два разных типизированных клиента зарегистрированы в общем интерфейсе, они оба будут повторно использовать один и тот же именованный клиент. Это может казаться так, как будто первый типизированный клиент получает второго именованного клиента, «неправильно» внедренного.

❌ НЕ регистрируйте несколько типизированных клиентов в одном интерфейсе без явного указания имени.

✔️ РЕКОМЕНДУЕТСЯ зарегистрировать и настроить именованный клиент отдельно, а затем связать его с одним или несколькими типизированными клиентами, указав имя в AddHttpClient<T> вызове или вызвав AddTypedClient во время настройки именованного клиента.

По задумке, регистрация и настройка именованного клиента с тем же именем несколько раз просто добавляет действия конфигурации в список уже существующих. Это поведение IHttpClientFactory может быть не очевидным, но это тот же подход, который используется шаблоном параметров и 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"));

См. также