0

I have a .tgz file that I need to download given a url inside a Testing folder. I am able to download the .tgz file successfully from the url using WebClient.

Below is my code:

private void DownloadTGZFile(string url, string fileToDownload)
{
    using (var client = new WebClient())
    {
        client.DownloadFile(url + fileToDownload, "Testing/configs.tgz");
    }
}

I wanted to see on how can I add a timeout to this call so that if url doesn't respond back within a particular time then it should timeout but it can retry for 3 times and then give up. Also I wanted to see on how can I use HttpClient here instead of WebClient considering it is an older BCL class and not recommended.

dragons
  • 549
  • 1
  • 8
  • 24
  • Does this answer your question? [How to change the timeout on a .NET WebClient object](https://stackoverflow.com/questions/1789627/how-to-change-the-timeout-on-a-net-webclient-object) – Hammas Jul 17 '20 at 14:28
  • 2
    For the retry part of the question check out https://www.nuget.org/packages/Polly – David Martin Jul 17 '20 at 14:34

2 Answers2

6

To download a file with HttpClient you can do:

// Is better to not initialize a new HttpClient each time you make a request, 
// it could cause socket exhaustion
private static HttpClient _httpClient = new HttpClient()
{
    Timeout = TimeSpan.FromSeconds(5)
};

public async Task<byte[]> GetFile(string fileUrl)
{
    using (var httpResponse = await _httpClient.GetAsync(fileUrl))
    {
        // Throws an exception if response status code isn't 200
        httpResponse.EnsureSuccessStatusCode();
        return await httpResponse.Content.ReadAsByteArrayAsync();
    }
}

For more details about socket exhaustion with HttpClient

As you see, to define a timeout for the Http call you should set a timeout while creating a new HttpClient.


To implement a retry policy for the previous code, I would install Polly NuGet package and then:

public async Task<byte[]> GetFile(string fileUrl)
{
    return await Policy
       .Handle<TaskCanceledException>() // The exception thrown by HttpClient when goes in timeout
       .WaitAndRetryAsync(retryCount: 3, sleepDurationProvider: i => TimeSpan.FromMilliseconds(300))
       .ExecuteAsync(async () =>
       {
           using (var httpResponse = await _httpClient.GetAsync(fileUrl))
           {
               // Throws an exception if response status code isn't 200
               httpResponse.EnsureSuccessStatusCode();
               return await httpResponse.Content.ReadAsByteArrayAsync();
           }
       });
}

In this case I defined a retry of 3 times with an interval of 300 milliseconds between each tentative. Also note that I didn't defined the retry for every kind of Exception, because if - for example - you put an invalid URL, retrying is nonsense.

At the end, if you want to save that byte array to a file, you can just do:

File.WriteAllBytes(@"MyPath\file.extension", byteArray);
Francesco Bonizzi
  • 5,142
  • 6
  • 49
  • 88
  • 1
    Thanks for your suggestion. It makes sense now and I understand how to use timeout on HttpClient in a better way now. In your example you are returning `byte[]` so how can I download this file (.tgz file) in a folder? In my case I need to download the file from url in a particular folder. Is this possible here? – dragons Jul 17 '20 at 15:24
  • 1
    I added how to save the file at the end of the answer! – Francesco Bonizzi Jul 17 '20 at 16:31
0

You can use this function with no dependencies to external libraries. It works for any file size.

EDIT Version to progapate the TaskCanceledException.

public async Task<bool> DownloadFileAsync(string url,
    string destinationFile,
    TimeSpan timeout,
    int maxTries = 3,
    CancellationToken token = default)
{
    using (var client = new HttpClient { Timeout = timeout })
    {
        for (var i = 0; i < maxTries; i++, token.ThrowIfCancellationRequested())
        {
            try
            {
                var response = await client.GetAsync(url, token);
                if (!response.IsSuccessStatusCode)
                    continue;

                var responseStream = await response.Content.ReadAsStreamAsync();
                using (var outputStream = new FileStream(destinationFile, FileMode.Create, FileAccess.Write))
                {
                    await responseStream.CopyToAsync(outputStream, 8 * 1024, token);
                    return true;
                }
            }
            catch (HttpRequestException)
            {
                //ignore
            }
        }
        return false;
    }
}
denys-vega
  • 3,522
  • 1
  • 19
  • 24
  • Did you forgot the `await` before the `Task.WaitAny(getTask);`? – Theodor Zoulias Jul 17 '20 at 17:10
  • Propagating cancellation by a special return value is unconventional. The [standard](https://learn.microsoft.com/en-us/dotnet/standard/threading/cancellation-in-managed-threads) way is to throw an `OperationCanceledException`. – Theodor Zoulias Jul 17 '20 at 17:13
  • @TheodorZoulias this function is not async, please check the [MSDN](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.waitany?view=netcore-3.1#System_Threading_Tasks_Task_WaitAny_System_Threading_Tasks_Task___) – denys-vega Jul 17 '20 at 18:18
  • Yes, you are right, it's not asynchronous indeed. Why don't you use the `Task.WhenAny`, to avoid blocking the thread? – Theodor Zoulias Jul 17 '20 at 19:14
  • Checking the `token.IsCancellationRequested` results to inconsistent behavior. I suggest to use the `token.ThrowIfCancellationRequested()` instead. – Theodor Zoulias Jul 17 '20 at 19:17
  • 1
    @TheodorZoulias I've removed the first function to avoid confusions and unconventional code and I updated the second one. Thanks for your suggestions. – denys-vega Jul 17 '20 at 22:13
  • Don't initialize your HttpClient every time you call the DownFileAsync method. Instead make it globally static like Francesco's answer above (or even better even use HttpClientFactory to create the HttpClient instance). Otherwise you risk socket exhaustion (also mentioned in Francesco's answer) – bytedev Mar 11 '22 at 07:08