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