Поделиться через


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

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

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

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

HttpClient не уважает Scoped время существования

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

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

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

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

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

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

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

✔️ Рекомендуется не хранить информацию или передавать ее только в экземпляре HttpRequestMessage .

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

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

Чтобы создать только HttpMessageHandler без, вызов IHttpMessageHandlerFactory.CreateHandler любого зарегистрированного именованного клиентаHttpClient. В этом случае необходимо создать 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 экземпляр внедряется в конструктор, поэтому он будет совместно использовать типизированное время существования клиента.

Дополнительные сведения см. в разделах 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. Transient Регистрирует службу с помощью предоставленной или TClient,TImplementation предоставленной TClient службы.

Следующие два оператора технически одинаковы:

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 Это происходит из-за того, что "по умолчанию" не настроен 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"));

См. также