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).

11
  • From the documentation: 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. Commented Oct 13, 2019 at 16:22
  • Actually, it IS instantiated once.. There is a single request in the program, then it exits. Commented Oct 13, 2019 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? Commented Oct 13, 2019 at 17:37
  • I put a breakpoint there but it is never called. Commented Oct 13, 2019 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. Commented Oct 13, 2019 at 17:43

2 Answers 2

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).

Sign up to request clarification or add additional context in comments.

4 Comments

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: github.com/nagylzs/csharp_http_post_example/blob/master/… but it is also not called. At least for me.
@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.
It was not for me. Also put a breakpoint at "throw new Exception" line but it is never executed.
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.
-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.

7 Comments

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 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.
@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).
After adding ConfigureAwait(false) to all await expressions, it is still blocked.
@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.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.