Udostępnij za pośrednictwem


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 HttpRequestMessageobiektu , 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 IHttpClientFactoryelement , i użyj jej do opakowania utworzonej IHttpClientFactoryprocedury 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 HttpMessageHandlerelementó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ć nowych HttpClient 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ęcia HttpMessageHandlerobiektu . IHttpClientFactory śledzi i usuwa zasoby używane do tworzenia HttpClient wystąpień, w szczególności HttpMessageHandler wystąpień, gdy tylko ich okres istnienia wygaśnie i nie HttpClient 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ę TClienttypu . 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:

  1. Rejestruje nazwanego klienta (w prostym przypadku domyślnym nazwa to typeof(TClient).Name).
  2. Rejestruje usługę Transient przy użyciu usługi lub TClient,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 HttpClientFactoryprzez element , powodując przerwanie linku do nazwanego klienta. Będzie ona manifestować tak, jakby HttpClientkonfiguracja 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"));

Zobacz też