2

I have a .NET framework Windows Forms application with a form that has this code:

using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace test
{

    public partial class Main : Form
    {

        public int exitCode = 1;
        private Options opts;
        CancellationTokenSource cancellationSource = new CancellationTokenSource();


        public Main(Options opts)
        {
            InitializeComponent();
            this.opts = opts;
        }

        private void btnCancel_Click(object sender, EventArgs e)
        {
            exitCode = 1;
            cancellationSource.Cancel();
            Close();
        }

        async Task doUpload()
        {
            using (var content = new MultipartFormDataContent())
            {
                List<FileStream> streams = new List<FileStream>();
                try
                {
                    foreach (string fPath in opts.InputFiles)
                    {
                        FileStream stream = new FileStream(fPath, FileMode.Open, FileAccess.Read);
                        streams.Add(stream);
                        content.Add(new StreamContent(stream), fPath);
                    }
                    var progressContent = new ProgressableStreamContent(
                         content,
                         4096,
                         (sent, total) =>
                         {
                             double percent = 100 * sent / total;
                             progressBar.Value = (int)percent;
                         });

                    using (var client = new HttpClient())
                    {
                        using (var response = await client.PostAsync(opts.URL, progressContent, cancellationSource.Token))
                        {
                            if (response.IsSuccessStatusCode)
                            {
                                exitCode = 0;
                            }
                            else
                            {
                                MessageBox.Show(
                                    response.Content.ToString(),
                                    "Error " + response.StatusCode,
                                    MessageBoxButtons.OK, MessageBoxIcon.Error
                               );
                            }
                            Close();
                        }
                    }
                }
                finally
                {
                    foreach (FileStream stream in streams)
                    {
                        stream.Close();
                    }
                }
            }

        }

        private void Main_Load(object sender, EventArgs e)
        {
        }

        private void Main_FormClosing(object sender, FormClosingEventArgs e)
        {
            e.Cancel = !cancellationSource.IsCancellationRequested;
        }

        private void Main_Shown(object sender, EventArgs e)
        {
            doUpload();
        }
    }
}

The ProgressableStreamContent is the same that was given here: C#: HttpClient, File upload progress when uploading multiple file as MultipartFormDataContent

The problem is that the response is never returned. In other words: await for postAsync never completes. Also, the progress callback is never called back. Even if I try to use a POST URL that contains a non-exsitent domain, nothing happens. I guess it is a deadlock, but I don't see how? The async Task's result is never used anywhere and it is not awaited for.

It is different from An async/await example that causes a deadlock because .Result is not used and the method is never awaited for, and also it seems that calling ConfigureAwait(false) ha no effect.

UPDATE: I have created a new github repo for this question, so anyone can test it:

https://github.com/nagylzs/csharp_http_post_example

UPDATE: Finally it works. ConfigureAwait is not needed. All UI update operations must be placed inside Invoke. I have updated the test repo to the working version. Also added TLSv1.2 support (which is disabled by default).

nagylzs
  • 3,954
  • 6
  • 39
  • 70
  • From the [documentation](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient): *`HttpClient` is intended to be instantiated once and re-used throughout the life of an application. Instantiating an HttpClient class for every request will exhaust the number of sockets available under heavy loads.* – Theodor Zoulias Oct 13 '19 at 16:22
  • Actually, it IS instantiated once.. There is a single request in the program, then it exits. – nagylzs Oct 13 '19 at 16:52
  • You are right. Have you tried removing the `ProgressableStreamContent` from the equation, or commenting the `progressBar.Value = (int)percent;` line, to see what happens? – Theodor Zoulias Oct 13 '19 at 17:37
  • I put a breakpoint there but it is never called. – nagylzs Oct 13 '19 at 17:40
  • Don't trust the breakpoints, because you have to do with multithreaded code. It's better to change the code and see what happens IMHO. – Theodor Zoulias Oct 13 '19 at 17:43
  • My guess is that the `ProgressableStreamContent` callback is not invoked in the UI thread, so you may have to use the [Control.Invoke](https://stackoverflow.com/a/661662/11178549) method to fix this. – Theodor Zoulias Oct 13 '19 at 17:43
  • Possible duplicate of [An async/await example that causes a deadlock](https://stackoverflow.com/questions/15021304/an-async-await-example-that-causes-a-deadlock) – vasil oreshenski Oct 13 '19 at 19:11
  • I'll try to put it into a different thread and use Control.Invoke (tomorrow) – nagylzs Oct 13 '19 at 19:49
  • Never? Even after 100 seconds? (the default timeout is 100 seconds) Are you sure the URL just isn't responding? – Gabriel Luci Oct 14 '19 at 00:32
  • I have purposely tried with an invalid/nonexistent domain without luck. That should return nxdomain/resolve error immediately. – nagylzs Oct 14 '19 at 05:04
  • Added ConfigureAwait(false) to all awaits (including ProgressableStreamContent) and replace the progress bar value update with "throw new Exception("FooBar")" but it is still blocked. – nagylzs Oct 14 '19 at 16:22

2 Answers2

2

PostAsync in the code you've posted doesn't block (but it really never returns though!). It throws an exception:

System.InvalidOperationException: Cross-thread operation not valid: Control 'progressBar' accessed from a thread other than the thread it was created on.

That's the reason for the breakpoints that didn't worked for you. The right solution would be:

var progressContent = new ProgressableStreamContent(
     content,
     4096,
     (sent, total) =>
     {
         Invoke((Action) (() => {
             double percent = 100 * sent / total;
             progressBar.Value = (int) percent;
         }));
     });

(either add Invoke or BeginInvoke to the callback)

The callbacks of the HTTP client are called on a background thread, and you have to put them into your window's even queue if you want them to access your UI controls.

.ConfigureAwait(false) has nothing to do with this issue, you shouldn't use it in UI context (quite the opposite: you want it to put the continuation onto the UI thread, so you shouldn't use it).

ForNeVeR
  • 6,726
  • 5
  • 24
  • 43
  • The example in the repo did not set the progresbar's value. By the time I have uploaded the repo, it was already replaced with a test exception. See here: https://github.com/nagylzs/csharp_http_post_example/blob/master/Main.cs#L51 but it is also not called. At least for me. – nagylzs Oct 16 '19 at 17:59
  • @nagylzs Could you please check again? Or even better: surround the whole method with `try/catch` and show the exception. I have tested the code in the repo, and the test exception was thrown. – ForNeVeR Oct 17 '19 at 08:20
  • It was not for me. Also put a breakpoint at "throw new Exception" line but it is never executed. – nagylzs Oct 31 '19 at 06:57
  • 1
    I have tried on a different computer today and it was working! Probably it is not a problem with the source code. You were right about using Invoke. configureAwait(false) is not needed. – nagylzs Oct 31 '19 at 09:42
-2

You need to change this:

client.PostAsync(opts.URL, progressContent, cancellationSource.Token)

to

client.PostAsync(opts.URL, progressContent, cancellationSource.Token).ConfigureAwait(false)

This is already discussed so you can find additional resources on the net, but this should be good starting point.

vasil oreshenski
  • 2,788
  • 1
  • 14
  • 21
  • Actually, I have tried this before posting the question and it did not work. The task is never awaited for, and its .Result is never used. – nagylzs Oct 13 '19 at 19:45
  • @nagylzs If you are using the same impl. as from the SO post you have posted, then i think this await content.ReadAsStreamAsync() also should have .ConfigureAwait(false), also any other use of await should have that configuration. Can you try to rework ProgressableStreamContent and try again. – vasil oreshenski Oct 13 '19 at 20:42
  • @nagylzs I undestand there is no blocking operation (.Result or .Wait()) but using .ConfigrueAwait(false) where async is used will show if the problem is really related to async / await, the other thing you can try is to ditch async / await and work directly with .ContinueWith. If the issue is resolved using ContinueWith we can be sure that the problem is related to dead lock. Or you can use some tool for dead lock detection (there are few paid, but have trail periods). – vasil oreshenski Oct 14 '19 at 08:09
  • After adding ConfigureAwait(false) to all await expressions, it is still blocked. – nagylzs Oct 14 '19 at 16:21
  • @nagylzs Well, the other option is some of the files you are streaming to have locks on them, so when the content needs to be streamed which will be on .PostAsync the lock on the file will hold the process. You can try to remove the StreamContent and the FileStream and use ByteArrayContent. If there is lock on the file the blocking will happen when you are tring to read the file content. I can't think of anything else at the moment. – vasil oreshenski Oct 14 '19 at 20:49
  • The whole reason for posting this way was to be able to post huge files without loading them into memory. Using a ByteArrayContent is not an option. The test file I was trying to stream is a 1KB ini file, and it was surely not locked by the OS. You can clone the git repo and try if you want. I appriciate your help, and I'm going to accept your answer if it really works. But meanwhile, I'm going to use a different language to implement this because it must be ready today. – nagylzs Oct 15 '19 at 05:26
  • @nagylzs Sure, no worries. I did not saw the repo. Later i will try it myself and will report back here. – vasil oreshenski Oct 15 '19 at 07:52