Typowe IHttpClientFactory
problemy z użyciem
W tym artykule poznasz niektóre z najczęstszych problemów, które można napotkać podczas tworzenia IHttpClientFactory
HttpClient
wystąpień.
IHttpClientFactory
to wygodny sposób konfigurowania wielu HttpClient
konfiguracji w kontenerze DI, konfigurowania rejestrowania, konfigurowania strategii odporności i nie tylko. IHttpClientFactory
Hermetyzuje również zarządzanie HttpClient
okresem istnienia wystąpień i HttpMessageHandler
, aby zapobiec problemom, np. wyczerpaniu gniazd i utracie zmian DNS. Aby zapoznać się z omówieniem sposobu używania IHttpClientFactory
w aplikacji platformy .NET, zobacz IHttpClientFactory with .NET (IHttpClientFactory z platformą .NET).
Ze względu na złożony charakter IHttpClientFactory
integracji z di, można napotkać niektóre problemy, które mogą być trudne do przechwycenia i rozwiązywania problemów. Scenariusze wymienione w tym artykule zawierają również zalecenia, które można proaktywnie zastosować, aby uniknąć potencjalnych problemów.
HttpClient
nie szanuje Scoped
okresu istnienia
Możesz napotkać problem, jeśli musisz uzyskać dostęp do dowolnej usługi o określonym zakresie, na przykład HttpContext
, lub niektórych pamięci podręcznych o określonym zakresie, z poziomu HttpMessageHandler
. Zapisane tam dane mogą "zniknąć" lub w drugą stronę "utrwalać", gdy nie powinny. Jest to spowodowane niezgodnością zakresu wstrzykiwania zależności (DI) między kontekstem aplikacji a wystąpieniem programu obsługi i znanym ograniczeniem w programie IHttpClientFactory
.
IHttpClientFactory
Tworzy oddzielny zakres di dla każdego HttpMessageHandler
wystąpienia. Te zakresy obsługi różnią się od zakresów kontekstu aplikacji (na przykład zakresu żądań przychodzących ASP.NET Core lub zakresu ręcznego di utworzonego przez użytkownika), więc nie będą współużytkowane wystąpienia usługi o określonym zakresie.
W wyniku tego ograniczenia:
- Wszystkie dane buforowane "zewnętrznie" w usłudze o określonym zakresie nie będą dostępne w ramach .
HttpMessageHandler
- Wszelkie dane buforowane "wewnętrznie" w ramach
HttpMessageHandler
lub jego zależności o określonym zakresie można zaobserwować z wielu zakresów di aplikacji (na przykład z różnych żądań przychodzących), ponieważ mogą one współużytkować tę samą procedurę obsługi.
Rozważ następujące zalecenia, aby ułatwić złagodzenie tego znanego ograniczenia:
❌ NIE buforuj żadnych informacji związanych z zakresem (takich jak dane z HttpContext
) wewnątrz HttpMessageHandler
wystąpień lub jego zależności, aby uniknąć wycieku poufnych informacji.
❌ Nie używaj plików cookie, ponieważ CookieContainer
będą one udostępniane razem z programem obsługi.
✔️ ROZWAŻ, aby nie przechowywać informacji lub przekazywać je tylko w ramach HttpRequestMessage
wystąpienia.
Aby przekazać dowolne informacje obok HttpRequestMessage
obiektu , możesz użyć HttpRequestMessage.Options właściwości .
✔️ ROZWAŻ hermetyzowanie całej logiki powiązanej z zakresem (na przykład uwierzytelnianie) w osobnej oddzielnych DelegatingHandler
elementach, które nie zostały utworzone przez IHttpClientFactory
element , i użyj jej do opakowania utworzonej IHttpClientFactory
procedury obsługi.
Aby utworzyć tylko element bez , wywołaj IHttpMessageHandlerFactory.CreateHandler każdą zarejestrowaną HttpMessageHandler
nazwę klienta.HttpClient
W takim przypadku należy utworzyć HttpClient
wystąpienie samodzielnie przy użyciu połączonej procedury obsługi. Ten obejście można znaleźć w pełni możliwe do uruchomienia w witrynie GitHub.
Aby uzyskać więcej informacji, zobacz sekcję Zakresy programu obsługi komunikatów w temacie IHttpClientFactory w IHttpClientFactory
wytycznych.
HttpClient
nie uwzględnia zmian DNS
Nawet jeśli IHttpClientFactory
jest używany, nadal można napotkać nieaktualny problem DNS. Zwykle może się tak zdarzyć, jeśli HttpClient
wystąpienie zostanie przechwycone w Singleton
usłudze lub, ogólnie, przechowywane gdzieś przez dłuższy czas niż określony HandlerLifetime
. HttpClient
zostanie również przechwycony, jeśli odpowiedni typowany klient zostanie przechwycony przez pojedynczy klient.
❌ NIE buforuj HttpClient
wystąpień utworzonych przez IHttpClientFactory
dłuższy czas.
❌ NIE należy wprowadzać typowych wystąpień klienta do Singleton
usług.
✔️ ROZWAŻ zażądanie klienta w IHttpClientFactory
odpowiednim czasie lub za każdym razem, gdy jest potrzebny. Klienci utworzoni w fabryce są bezpieczni do usunięcia.
HttpClient
wystąpienia utworzone przez IHttpClientFactory
program mają być krótkotrwałe.
Odtwarzanie i ponowne tworzenie
HttpMessageHandler
elementów po wygaśnięciu ich okresu istnienia jest niezbędne,IHttpClientFactory
aby zapewnić, że programy obsługi reagują na zmiany DNS.HttpClient
program jest powiązany z określonym wystąpieniem programu obsługi podczas jego tworzenia, dlatego należy zażądać nowychHttpClient
wystąpień w odpowiednim czasie, aby upewnić się, że klient uzyska zaktualizowaną procedurę obsługi.Usunięcie takich
HttpClient
wystąpień utworzonych przez fabrykę nie doprowadzi do wyczerpania gniazd, ponieważ jego usuwanie nie powoduje usunięciaHttpMessageHandler
obiektu .IHttpClientFactory
śledzi i usuwa zasoby używane do tworzeniaHttpClient
wystąpień, w szczególnościHttpMessageHandler
wystąpień, gdy tylko ich okres istnienia wygaśnie i nieHttpClient
będzie już ich używać.
Typowani klienci mają być również krótkotrwałi , ponieważ HttpClient
wystąpienie jest wstrzykiwane do konstruktora, więc będzie współużytkować typowany okres istnienia klienta .
Aby uzyskać więcej informacji, zobacz HttpClient
zarządzanie okresem istnienia i Unikaj wpisywanych klientów w sekcjach usług pojedynczych w IHttpClientFactory
wytycznych.
HttpClient
używa zbyt wielu gniazd
Nawet jeśli IHttpClientFactory
jest używany, nadal można napotkać problem z wyczerpaniem gniazd w określonym scenariuszu użycia. Domyślnie HttpClient
nie ogranicza liczby współbieżnych żądań. Jeśli jednocześnie jest uruchamiana duża liczba żądań HTTP/1.1, każda z nich spowoduje wyzwolenie nowej próby połączenia HTTP, ponieważ w puli nie ma bezpłatnego połączenia i nie ustawiono żadnego limitu.
❌ Nie uruchamiaj jednocześnie dużej liczby żądań HTTP/1.1 jednocześnie bez określania limitów.
✔️ ROZWAŻ ustawienie HttpClientHandler.MaxConnectionsPerServer (lub SocketsHttpHandler.MaxConnectionsPerServer, jeśli używasz go jako podstawowego programu obsługi) do rozsądnej wartości. Należy pamiętać, że te limity dotyczą tylko określonego wystąpienia programu obsługi.
✔️ ROZWAŻ użycie protokołu HTTP/2, który umożliwia multipleksowanie żądań za pośrednictwem pojedynczego połączenia TCP.
Typowany klient ma nieprawidłowe wstrzyknięcie HttpClient
Mogą wystąpić różne sytuacje, w których można uzyskać nieoczekiwane HttpClient
wstrzyknięcie do typizowanego klienta. W większości przypadków główna przyczyna będzie w błędnej konfiguracji, ponieważ zgodnie z projektem di każda kolejna rejestracja usługi zastępuje poprzednią.
Typowi klienci używają nazwanych klientów "pod maską": dodawanie typizowanego klienta niejawnie rejestruje i łączy je z nazwanym klientem. Nazwa klienta, chyba że podano jawnie, zostanie ustawiona na nazwę TClient
typu . Byłoby to pierwsze z TClient,TImplementation
pary, jeśli AddHttpClient<TClient,TImplementation>
są używane przeciążenia.
W związku z tym zarejestrowanie wpisanego klienta wykonuje dwie oddzielne czynności:
- Rejestruje nazwanego klienta (w prostym przypadku domyślnym nazwa to
typeof(TClient).Name
). - Rejestruje usługę
Transient
przy użyciu usługi lubTClient,TImplementation
udostępnionejTClient
.
Następujące dwa oświadczenia są technicznie takie same:
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
W prostym przypadku będzie ona również podobna do następującej:
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))));
Rozważmy następujące przykłady sposobu, w jaki połączenie między typami i nazwanymi klientami może być przerwane.
Typowany klient jest rejestrowany po raz drugi
❌ Nie rejestruj wpisanego klienta oddzielnie — jest on już automatycznie rejestrowany przez wywołanie AddHttpClient<T>
.
Jeśli typowany klient jest błędnie zarejestrowany po raz drugi jako zwykła usługa przejściowa, spowoduje to zastąpienie rejestracji dodanej HttpClientFactory
przez element , powodując przerwanie linku do nazwanego klienta. Będzie ona manifestować tak, jakby HttpClient
konfiguracja została utracona, ponieważ nieskonfigurowane HttpClient
zostaną wprowadzone do typizowanego klienta .
Może być mylące, że zamiast zgłaszać wyjątek, jest używany "niewłaściwy". HttpClient
Dzieje się tak, ponieważ "domyślne" nieskonfigurowane HttpClient
— klient o Options.DefaultName nazwie (string.Empty
) — jest zarejestrowany jako zwykła usługa przejściowa, aby umożliwić najbardziej podstawowy HttpClientFactory
scenariusz użycia. Dlatego po przerwaniu linku i typizowanego klienta stanie się tylko zwykłą usługą, ta "domyślna" HttpClient
zostanie naturalnie wstrzyknięta do odpowiedniego parametru konstruktora.
W typowym interfejsie są rejestrowane różne typy klientów
W przypadku zarejestrowania dwóch różnych klientów typowych w wspólnym interfejsie obaj używają tego samego nazwanego klienta. Może to wydawać się, że pierwszy typdowany klient otrzymuje drugi nazwany klient "błędnie" wstrzyknięty.
❌ NIE rejestruj wielu typiowanych klientów w jednym interfejsie bez jawnego określenia nazwy.
✔️ ROZWAŻ zarejestrowanie i skonfigurowanie nazwanego klienta oddzielnie, a następnie połączenie go z jednym lub wieloma typionymi klientami przez określenie nazwy w AddHttpClient<T>
wywołaniu lub wywołanie AddTypedClient
podczas konfiguracji nazwanego klienta .
Zgodnie z projektem rejestrowanie i konfigurowanie nazwanego klienta o tej samej nazwie kilka razy dołącza akcje konfiguracji do listy istniejących. To zachowanie HttpClientFactory
może nie być oczywiste, ale jest to takie samo podejście, które jest używane przez wzorzec opcji i interfejsy API konfiguracji, takie jak Configure.
Jest to głównie przydatne w przypadku zaawansowanych konfiguracji programu obsługi, na przykład dodanie niestandardowej procedury obsługi do nazwanego klienta zdefiniowanego zewnętrznie lub wyśmiewanie podstawowej procedury obsługi dla testów, ale działa również na potrzeby HttpClient
konfiguracji wystąpienia. Na przykład trzy poniższe przykłady spowodują HttpClient
skonfigurowanie w taki sam sposób (zarówno BaseAddress
jak i DefaultRequestHeaders
ustawione):
// 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"));
Umożliwia to łączenie typizowanego klienta z już zdefiniowanym nazwanym klientem, a także łączenie kilku typowych klientów z jednym nazwanym klientem. Jest to bardziej oczywiste, gdy są używane przeciążenia z parametrem name
:
services.AddHttpClient("LogClient", c => c.BaseAddress = new Uri(LogServerAddress));
services.AddHttpClient<FooLogger>("LogClient");
services.AddHttpClient<BarLogger>("LogClient");
To samo można również osiągnąć, wywołując wywołanie AddTypedClient podczas konfiguracji nazwanego klienta :
services.AddHttpClient("LogClient", c => c.BaseAddress = new Uri(LogServerAddress))
.AddTypedClient<FooLogger>()
.AddTypedClient<BarLogger>();
Jeśli jednak nie chcesz ponownie używać tego samego nazwanego klienta, ale nadal chcesz zarejestrować klientów w tym samym interfejsie, możesz to zrobić, jawnie określając dla nich różne nazwy:
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"));