Condividi tramite


Problemi comuni relativi all'utilizzo di IHttpClientFactory

In questo articolo verranno illustrati alcuni dei problemi più comuni riscontrabili durante l'utilizzo di IHttpClientFactory per creare istanze HttpClient.

IHttpClientFactory è un modo pratico per impostare più configurazioni HttpClient nel contenitore di inserimento delle dipendenze, configurare la registrazione, elaborare strategie di resilienza e altro ancora. IHttpClientFactory incapsula anche la gestione della durata delle HttpClient istanze e HttpMessageHandler per evitare problemi come l'esaurimento del socket e la perdita di modifiche DNS. Per una panoramica su come usare IHttpClientFactory nell'applicazione .NET, vedere IHttpClientFactory con .NET.

A causa di una natura complessa dell'integrazione di IHttpClientFactory con l'inserimento delle dipendenze, è possibile riscontrare alcuni problemi difficili da intercettare e risolvere. Gli scenari elencati in questo articolo contengono anche raccomandazioni applicabili in modo proattivo per evitare potenziali problemi.

HttpClient non rispetta la durata di Scoped

È possibile riscontrare un problema quando serve accedere a qualsiasi servizio con ambito, come HttpContext o a qualche cache con ambito, dall'interno di HttpMessageHandler. I dati salvati possono "scomparire" o, in altro modo, "persistere" quando non dovrebbero. Ciò dipende dalla mancata corrispondenza dell'ambito dell'inserimento delle dipendenze (Dependency Injection, DI) tra il contesto dell'applicazione e l'istanza del gestore; è un limite noto in IHttpClientFactory.

IHttpClientFactory crea un ambito di inserimento delle dipendenze separato per ogni istanza HttpMessageHandler. Questi ambiti del gestore sono separati dagli ambiti del contesto dell'applicazione, ad esempio l’ambito delle richieste in ingresso ASP.NET Core, l’ambito di inserimento delle dipendenze manuale creato dall'utente, perciò non condivideranno le istanze di servizio di ambito.

Come conseguenza di questo limite:

  • Tutti i dati memorizzati nella cache "esternamente" in un servizio con ambito non saranno disponibili all'interno di HttpMessageHandler.
  • Tutti i dati memorizzati nella cache "internamente" all'interno di HttpMessageHandler o le relative dipendenze con ambito possono essere osservati da più ambiti di inserimento delle dipendenze dell'applicazione (ad esempio, da diverse richieste in ingresso) perché possono condividere lo stesso gestore.

Considerare i suggerimenti seguenti per attenuare questa limitazione nota:

❌ NON memorizzare nella cache le informazioni relative all'ambito (ad esempio i dati da HttpContext) all'interno di istanze HttpMessageHandler o delle relative dipendenze per evitare la divulgazione di informazioni riservate.

❌ NON usare i cookie, perché il CookieContainer verrà condiviso insieme al gestore.

✔️ CONSIDERARE di non archiviare le informazioni o di passarle solo all'interno dell'istanza HttpRequestMessage.

Per passare informazioni arbitrarie insieme a HttpRequestMessage, si può usare la proprietà HttpRequestMessage.Options.

✔️ CONSIDERARE l'incapsulamento di tutta la logica correlata all'ambito (ad esempio l'autenticazione) in un DelegatingHandler che non sia creato da IHttpClientFactory e di utilizzarlo per eseguire il wrapping del gestore creato da IHttpClientFactory.

Per creare solo un HttpMessageHandler senza HttpClient, chiamare IHttpMessageHandlerFactory.CreateHandler per ogni client denominato registrato. In tal caso, sarà necessario creare un'istanza HttpClient usando il gestore combinato. È disponibile un esempio completamente eseguibile per questa soluzione alternativa in GitHub.

Per altre informazioni, consultare la sezione Ambiti del gestore di messaggi in IHttpClientFactory nelle linee guida di IHttpClientFactory.

HttpClient non rispetta le modifiche al DNS

Anche se si usa IHttpClientFactory, è comunque possibile risolvere il problema del DNS non aggiornato. Ciò può verificarsi in genere se un'istanza HttpClient viene acquisita in un servizio Singleton o, in generale, archiviata in una posizione per un periodo di tempo più lungo rispetto al HandlerLifetime specificato. HttpClient verrà acquisito anche se il rispettivo client tipizzato viene acquisito da un singleton.

❌ NON memorizzare nella cache istanze HttpClient create da IHttpClientFactory per periodi di tempo prolungati.

❌ NON inserire istanze di client tipizzati nei servizi Singleton.

✔️ CONSIDERARE la richiesta di un client da IHttpClientFactory in modo tempestivo o ogni qual volta necessaria. I client creati dalla factory sono sicuri da eliminare.

Le istanze HttpClient create da IHttpClientFactory sono destinate a breve durata.

  • Il riciclo e la ricreazione di HttpMessageHandler quando la loro durata scade è essenziale per IHttpClientFactory, per garantire che i gestori reagiscano alle modifiche DNS. HttpClient è associato a un'istanza specifica del gestore al momento della creazione, pertanto le nuove istanze HttpClient devono essere richieste in modo tempestivo per garantire che il client ottenga il gestore aggiornato.

  • L'eliminazione di tali istanze HttpClient create dalla factory non comporta l'esaurimento del socket, perché lo smaltimento non attiverà l'eliminazione di HttpMessageHandler. IHttpClientFactory tiene traccia ed elimina le risorse usate per creare istanze HttpClient, in particolare le istanze HttpMessageHandler, non appena scade la durata e HttpClient non le usa più.

Anche i client tipizzati sono destinati a essere di breve durata, come un'istanza HttpClient che viene inserita nel costruttore, in modo che condivida la stessa durata del client tipizzato.

Per altre informazioni, vedere le sezioni HttpClient Gestione della durata ed Evitare client tipizzati nei servizi singleton nelle linee guida di IHttpClientFactory.

HttpClient usa troppi socket

Anche se si usa IHttpClientFactory, è comunque possibile riscontrare un problema di esaurimento dei socket con uno scenario di utilizzo specifico. Per impostazione predefinita, HttpClient non limita il numero di richieste simultanee. Se un numero elevato di richieste HTTP/1.1 viene avviato simultaneamente e contemporaneamente, ognuna di esse genererà un nuovo tentativo di connessione HTTP, perché nel pool non è presente alcuna connessione gratuita e non viene impostato alcun limite.

❌ NON avviare contemporaneamente un numero elevato di richieste HTTP/1.1 senza specificare i limiti.

✔️ CONSIDERARE l'impostazione di HttpClientHandler.MaxConnectionsPerServer (o di SocketsHttpHandler.MaxConnectionsPerServer, se lo si usa come gestore primario) su un valore ragionevole. Si noti che questi limiti si applicano solo alla specifica istanza del gestore.

✔️ CONSIDERARE l'uso di HTTP/2, che consente richieste multiplexing su una singola connessione TCP.

Il client tipizzato ha inserito il HttpClient sbagliato

Possono verificarsi diverse situazioni in cui è possibile ottenere un'inserimento imprevisto di HttpClient in un client tipizzato. Nella maggior parte dei casi, la causa principale sarà una configurazione errata, ad esempio, per progettazione dell'inserimento di dipendenze, qualsiasi registrazione successiva di un servizio sovrascrive quella precedente.

I client tipizzati usano client denominati "dietro le quinte": l'aggiunta di un client tipizzato registra in modo implicito e lo collega a un client denominato. Il nome del client, a meno che non sia fornito esplicitamente, verrà impostato sul nome di tipo di TClient. Si tratta del primo elemento della coppia di TClient,TImplementation se si usano gli overload di AddHttpClient<TClient,TImplementation>.

Pertanto, la registrazione di un client tipizzato esegue due operazioni separate:

  1. Registra un client denominato denominato (in un semplice caso predefinito, il nome è typeof(TClient).Name).
  2. Registra un servizio Transient usando il TClient o il TClient,TImplementation fornito.

Le due istruzioni seguenti sono tecnicamente identiche:

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

In un caso semplice, sarà anche simile al seguente:

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))));

Si considerino gli esempi seguenti di come il collegamento tra il client tipizzato e quello denominato può essere interrotto.

Il client tipizzato viene registrato una seconda volta

❌ NON registrare separatamente il client tipizzato: è già registrato automaticamente dalla chiamata AddHttpClient<T>.

Se un client tipizzato viene registrato per errore una seconda volta come normale servizio temporaneo, questa operazione sovrascriverà la registrazione aggiunta da HttpClientFactory, interrompendo così il collegamento con il client denominato. Si manifesterà come la configurazione di HttpClient fosse persa, perché un HttpClient non configurato verrà inserito nel client tipizzato.

Potrebbe confondere che, invece di generare un'eccezione, viene usato un HttpClient "errato". Ciò si verifica perché il HttpClient "predefinito" non configurato, ovvero il client con il nome Options.DefaultName (string.Empty) viene registrato come normale servizio temporaneo, per abilitare lo scenario di utilizzo più semplice di HttpClientFactory. Ecco perché dopo che il collegamento viene interrotto e il client tipizzato tipizzato diventa un semplice servizio ordinario, questo HttpClient "predefinito" verrà naturalmente inserito nel rispettivo parametro del costruttore.

Diversi client tipizzati sono registrati su un'interfaccia comune

Nel caso in cui due diversi client tipizzati fossero registrati su un'interfaccia comune, entrambi riutilizzerebbero lo stesso client denominato. Questo può sembrare il primo client tipizzato che riceve il secondo client denominato "erroneamente" inserito.

❌ NON registrare più client tipizzati in una singola interfaccia senza specificare in modo esplicito il nome.

✔️ CONSIDERARE la registrazione e la configurazione separata di un client denominato e poi il suo collegamento a uno o più clienti tipizzati o specificando il nome nella chiamata a AddHttpClient<T> o chiamando AddTypedClient durante la configurazione del client denominato.

Per impostazione predefinita, la registrazione e la configurazione di un client denominato con lo stesso nome spesso aggiunge semplicemente le operazioni di configurazione all'elenco di quelle esistenti. Questo comportamento di HttpClientFactory potrebbe non essere ovvio, ma è lo stesso approccio usato dal Modello di opzioni e dalle API di configurazione come Configure.

Ciò è particolarmente utile per le configurazioni avanzate del gestore, ad esempio l'aggiunta di un gestore personalizzato a un client denominato definito esternamente o la simulazione di un gestore primario per i test, ma funziona anche per la configurazione dell'istanza di HttpClient. Ad esempio, i tre esempi seguenti generano un HttpClient configurato nello stesso modo (sono impostati sia BaseAddress che 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"));

In questo modo è possibile collegare un client tipizzato a un client denominato già definito, ma anche collegare diversi client tipizzati a un singolo client denominato. È più ovvio quando si usano overload con un parametro name:

services.AddHttpClient("LogClient", c => c.BaseAddress = new Uri(LogServerAddress));

services.AddHttpClient<FooLogger>("LogClient");
services.AddHttpClient<BarLogger>("LogClient");

Lo stesso risultato può essere raggiunto anche chiamando AddTypedClient durante la configurazione del client denominato:

services.AddHttpClient("LogClient", c => c.BaseAddress = new Uri(LogServerAddress))
    .AddTypedClient<FooLogger>()
    .AddTypedClient<BarLogger>();

Tuttavia, se non si vuole riutilizzare lo stesso client denominato, ma ancora non si vuole registrare i client sulla stessa interfaccia, è possibile farlo specificando esplicitamente nomi diversi per i client:

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"));

Vedi anche