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 perIHttpClientFactory
, per garantire che i gestori reagiscano alle modifiche DNS.HttpClient
è associato a un'istanza specifica del gestore al momento della creazione, pertanto le nuove istanzeHttpClient
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 diHttpMessageHandler
.IHttpClientFactory
tiene traccia ed elimina le risorse usate per creare istanzeHttpClient
, in particolare le istanzeHttpMessageHandler
, non appena scade la durata eHttpClient
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:
- Registra un client denominato denominato (in un semplice caso predefinito, il nome è
typeof(TClient).Name
). - Registra un servizio
Transient
usando ilTClient
o ilTClient,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"));