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, configurare il logging, impostare 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 della natura complessa dell'integrazione di IHttpClientFactory con DI, è possibile riscontrare alcuni problemi difficili da individuare 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 uno scopo DI separato per ciascuna istanza HttpMessageHandler. Questi scopi dei gestori sono distinti dagli scopi del contesto dell'applicazione, come per esempio l'ambito delle richieste in ingresso di ASP.NET Core o l'ambito manuale di inserimento delle dipendenze creato dall'utente, e quindi non condivideranno le istanze di servizio a livello di ambito.

Come conseguenza di questo limite:

  • I dati memorizzati nella cache "esternamente" in un servizio con ambito limitato non saranno disponibili all'interno di HttpMessageHandler.
  • Dati memorizzati nella cache "internamente" all'interno di HttpMessageHandler o le relative dipendenze con ambito possono essere monitorati da più ambiti DI dell'applicazione (ad esempio, da diverse richieste in ingresso), in quanto condividono 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, poiché il CookieContainer sarà 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.

✔️ Valutare l'incapsulamento di tutta la logica correlata all'ambito (ad esempio, l'autenticazione) in un DelegatingHandler non creato da IHttpClientFactory, e usarlo per avvolgere il gestore creato da IHttpClientFactory.

Per creare solo un HttpMessageHandler senza HttpClient, chiamare IHttpMessageHandlerFactory.CreateHandler per qualsiasi client con nome registrato. In tal caso, sarà necessario creare un'istanza HttpClient utilizzando 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à catturato anche se il rispettivo client tipizzato viene catturato 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 di richiedere un client da IHttpClientFactory in modo tempestivo o ogni volta che ne hai bisogno. I client creati dalla fabbrica sono sicuri da eliminare.

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

  • Il riciclare e il ricreare HttpMessageHandler quando scade la loro durata è essenziale per IHttpClientFactory 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 HttpClientcreate dalla factory non comporta l'esaurimento di socket, poiché il loro smaltimento non attiva 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 ulteriori informazioni, consultare le sezioni HttpClient Gestione della durata ed Evitare clienti 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.

✔️ Considerate l'utilizzo di HTTP/2, che consente di multiplexare le richieste 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 poiché, per progettazione del Dependency Injection (DI), qualsiasi registrazione successiva di un servizio sovrascrive quella precedente.

I client tipizzati utilizzano client denominati "dietro le quinte": l'aggiunta di client tipizzati registra in modo implicito e li 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 TClient,TImplementation se si utilizzano i sovraccarichi di AddHttpClient<TClient,TImplementation>.

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

  1. Registra un client 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 IHttpClientFactory, 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 creare confusione il fatto che, invece di generare un'eccezione, venga utilizzato un HttpClient "sbagliato". 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 IHttpClientFactory. Ecco perché dopo che il collegamento viene interrotto e il client tipizzato diventa un semplice servizio ordinario, il "predefinito" HttpClient 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.

✔️ Valuta la possibilità di registrare e configurare separatamente un client nominato e poi collegarlo a uno o più client tipizzati, specificando il nome nella chiamata a AddHttpClient<T> o chiamando AddTypedClient durante la configurazione del client nominato.

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 IHttpClientFactory 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ù evidente quando vengono utilizzati i sovraccarichi con il 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 si vuole comunque registrare i client nella stessa interfaccia, è possibile farlo specificando in modo esplicito nomi diversi per essi:

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