Oggi mi è toccato risolvere uno strano problema di timeout che si è verificato improvvisamente a una delle mie applicazioni Web ASP.NET: quando ho guardato i log, ho capito facilmente che il timeout era causato da una richiesta HTTP emessa da un'istanza della classe System.Net.WebClient che non riusciva a connettersi a un servizio web di terze parti, il quale risultava temporaneamente non disponibile.
Ecco l'errore generato dall'oggetto WebClient:
System.Net.Sockets.SocketException: Impossibile stabilire la connessione. Risposta non corretta della parte connessa dopo l'intervallo di tempo oppure mancata risposta dall'host collegato 216.239.38.21:443 (server ip address).
E questo è lo snippet di codice che lo generava:
1 2 3 |
using (var wc = new WebClient()) { var result = wc.DownloadString("https://the.external.website"); } |
Ovviamente la chiamata di cui sopra si trovava all'interno di un blocco try / catch, che impediva all'applicazione web di andare in errore; al tempo stesso, però, l'impossibilità di effettuare la connessione provocava un timeout non indifferente, pari al timeout predefinito della classe WebClient... ovvero, 100.000 millisecondi (!), senza peraltro alcun modo di cambiarlo (!!) visto che l'oggetto WebRequest sottostante utilizzato per effettuare la richiesta web viene creato on-the-fly e quindi non esposto (!!!).
Non male come situazione, vero? Fortunatamente, dopo una breve ricerca su Google, ho trovato un thread su StackOverflow che mi ha consentito di gestire la situazione nel migliore dei modi. Il risultato delle mie ricerche è riassumibile nella la seguente sottoclasse, che ho chiamato RyadelWebClient:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
using System; using System.Net; using System.Threading.Tasks; namespace Ryadel.Components.Web { /// <summary> /// An extension of the standard System.Net.WebClient /// featuring a customizable constructor and [Timeout] property. /// ref.: https://www.ryadel.com/en/webclient-request-timeout-class-c-sharp-dot-net /// </summary> public class RyadelWebClient : WebClient { /// <summary> /// Default constructor (30000 ms timeout) /// NOTE: timeout can be changed later on using the [Timeout] property. /// </summary> public RyadelWebClient() : this(30000) { } /// <summary> /// Constructor with customizable timeout /// </summary> /// <param name="timeout"> /// Web request timeout (in milliseconds) /// </param> public RyadelWebClient(int timeout) { Timeout = timeout; } #region Methods protected override WebRequest GetWebRequest(Uri uri) { WebRequest w = base.GetWebRequest(uri); w.Timeout = Timeout; ((HttpWebRequest)w).ReadWriteTimeout = Timeout; return w; } public new async Task<string> DownloadStringTaskAsync(Uri address) { var t = base.DownloadStringTaskAsync(address); if (await Task.WhenAny(t, Task.Delay(Timeout)) != t) CancelAsync(); return await t; } #endregion /// <summary> /// Web request timeout (in milliseconds) /// </summary> public int Timeout { get; set; } } } |
Come si può facilmente vedere guardando il codice e confrontandolo con la risposta accettata su StackOverflow, ho provato a modificare un pò il suggerimento iniziale così da avere una classe leggermente più versatile: in questo modo è possibile impostare un timeout alle richieste web sincrone e asincrone sia a partire dal costruttore che impostando una proprietà apposita. Visto che c'ero, ho anche colto l'occasione per abbassare il valore di Timeout predefinito a 30 secondi, visto che 100 mi sembrava a dir poco esagerato come default value.
Conclusioni
Per il momento è tutto: mi auguro che questa classe possa essere utile ad altri sviluppatori che si troveranno a dover risolvere problemi di timeout similari legati alla classe System.Net.WebClient.