Runtime: WebClient.DownloadString() method hangs sometimes (Ubuntu)

Created on 9 Dec 2017  路  8Comments  路  Source: dotnet/runtime

WebClient.DownloadString() method hangs sometimes when called on Ubuntu platform. There is no system (at least I couldn't find it), but there are possibly many parallel HTTP requests called from PLINQ query, and the target sites are rather slow. In couple with the fact that .Core platform doesn't allow to abort threads, it leads to plenty of threads and retained data hanged forever.
Well, DownloadStringTaskAsync + Task.Delay somehow save the day, but there can be resource leakage when download task hangs.
All HttpWebRequest timeouts (Timeout and ReadWriteTimeout) are explicitly set to 1 minute.

Ubuntu 16.10 and 17.10
.NET Core 2.0.0

area-System.Net bug os-linux tenet-compatibility

Most helpful comment

Well, it looks like switching from WebClient to HttpClient helped.
By the way, WebClient leaves a lot of half-closed TCP connections (in CLOSE_WAIT state) even after it has been properly disposed. The obvious result is that eventually all available socket descriptors exhaust, and the process becomes unable to initiate a new connection.
This problem was observed by me only on Ubuntu; Windows version is OK.

All 8 comments

Can you share a minimal repro?

@karelz
I was using WebClient this way:

public abstract class WebDownloaderBase
{
   private static readonly ILogger M_Logger = LogManager.GetLogger("WebDownloader");

   static WebDownloaderBase()
   {
      ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
   }

   protected string DownloadString(string url, Encoding encoding = null, TimeSpan? timeout = null)
   {
      using (var webClient = new ExtendedWebClient(timeout))
      {
         if (encoding != null)
            webClient.Encoding = encoding;
         M_Logger.Debug($"GET {url}...");
         var response = webClient.DownloadString(url);  // *** Hangs here
         M_Logger.Debug($"GET {url} response:{Environment.NewLine}{response}");
         return response;
      }
   }


   private class ExtendedWebClient : WebClient
   {
      public bool SkipCertificateValidation { get; set; }

      private readonly TimeSpan m_Timeout;

      public ExtendedWebClient(TimeSpan? timeout)
      {
         m_Timeout = timeout.GetValueOrDefault(TimeSpan.FromSeconds(60));
      }

      protected override WebRequest GetWebRequest(Uri address)
      {
         var request = base.GetWebRequest(address) as HttpWebRequest;
         if (request == null)
            return null;
         if (SkipCertificateValidation)
            request.ServerCertificateValidationCallback = delegate { return true; };
         request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;
         request.Timeout = request.ReadWriteTimeout = (int)m_Timeout.TotalMilliseconds;
         return request;
      }
   }
}

Also, this problem occurs ONLY on the .NET Core platform. The previous version of the program was based on .NET Framework 4.5 and was executed with Mono without any problems.

Upd. Despite hanging, it seems that async equivalent (DownloadStringTaskAsync()) can be canceled by calling CancelAsync() method...

Upd.2 It seems to happen only on the HTTPS queries, maybe, the problem is in the SSL/TLS protocol implementation.

Upd.3 Windows 10 is also affected

I wrote a stress test, which calls into the WebDownloaderBase.DownloadString() to a HTTPS server, and not able to observe the hang. I'm using a Windows 10 machine with the current .NET Core code base.

Could you attach your repro/dump to help further investigation?

Original problem was reported on Ubuntu @caesar1995 and the handling may be different.

Well, it looks like switching from WebClient to HttpClient helped.
By the way, WebClient leaves a lot of half-closed TCP connections (in CLOSE_WAIT state) even after it has been properly disposed. The obvious result is that eventually all available socket descriptors exhaust, and the process becomes unable to initiate a new connection.
This problem was observed by me only on Ubuntu; Windows version is OK.

The difference is that HttpClient will reuse same connection(s) if possible so you end up open less.
The duration of CLOSE_WAIT is tunable on Linux and it is maintained by kernel (and not .net)
CLOSE_WAIT can also be a bug in .net or your application. It is hard to guess without complete repro case.

@wfurt: No, this is not the case, because I'm establishing a lot of connections to different servers from multiple threads. Besides, I dispose HttpClient after each usage, so the connection closes immediately.
Indeed, the duration of CLOSE_WAIT can be tweaked by the OS config, but it has no effect - libcurl, which is used by the .NET Core HTTP API, has own TCP timeouts and ignores the system ones.

By the way, WebClient leaves a lot of half-closed TCP connections (in CLOSE_WAIT state) even after it has been properly disposed. The obvious result is that eventually all available socket descriptors exhaust, and the process becomes unable to initiate a new connection.

The WebClient class on .NET Core was written as a compatibility class to help developers porting code from .NET Framework. WebClient is implemented on top of HttpWebRequest. And on .NET Core, HttpWebRequest is implemented on top of HttpClient. Due to this, the .NET Core implementation of WebClient will have poor performance characteristics. It will be not be able to pool TCP connections and will end up creating a new connection for every WebClient request.

Well, it looks like switching from WebClient to HttpClient helped.

New development should use HttpClient APIs as the preferred solution. It is also more performant in terms of TCP connection pooling as you noticed.

Was this page helpful?
0 / 5 - 0 ratings