3

I have been battling this for days now and can't figure it out or why it is the way it is. I am hoping one of you masterminds can explain it to me.

This is the code in question:

public async static Task<RemoteFileDetails> GetRemoteFileDetailsAsync(string strUrl, double dblTimeoutSeconds = 15.0)
{
    try
    {
        using (var hc = new HttpClient())
        {
            hc.Timeout = TimeSpan.FromSeconds(dblTimeoutSeconds);
            using (var h = new HttpRequestMessage(HttpMethod.Head, strUrl))
            {
                h.Headers.UserAgent.ParseAdd(UserAgent);
                using (var rm = await hc.SendAsync(h))
                {
                    rm.EnsureSuccessStatusCode();
                    return new RemoteFileDetails()
                    {
                        Url = strUrl,
                        FileName = rm.Content.Headers.ContentDisposition.FileName,
                        FileSize = rm.Content.Headers.ContentLength.GetValueOrDefault(),
                        LastModified = rm.Content.Headers.LastModified.GetValueOrDefault().LocalDateTime,
                        Valid = true
                    };
                }
            }
        }
    }
    catch (Exception ex)
    {
        System.Windows.MessageBox.Show(ex.ToString());
    }
    return new RemoteFileDetails();
}

All this does is puts in a HEAD request on a URL and grabs the certain header results.

The issue here is about 50% of the time it deadlocks (pauses for 60 seconds or so, then throws a TaskCanceledException), the other 50% it works wonderfully. Now, I added network logging, and this is what's happening on deadlock:

    System.Net Verbose: 0 : [2928] HttpWebRequest#3567399::HttpWebRequest(https://www.example.com/file.txt#-114720138)
    System.Net Information: 0 : [2928] Current OS installation type is 'Client'.
    System.Net Information: 0 : [2928] RAS supported: True
    System.Net Verbose: 0 : [2928] Exiting HttpWebRequest#3567399::HttpWebRequest() 
    System.Net Verbose: 0 : [2928] HttpWebRequest#3567399::HttpWebRequest(uri: 'https://www.example.com/file.txt', connectionGroupName: '565144')
    System.Net Verbose: 0 : [2928] Exiting HttpWebRequest#3567399::HttpWebRequest() 
    System.Net Verbose: 0 : [5456] HttpWebRequest#3567399::BeginGetResponse()
    System.Net Verbose: 0 : [2244] HttpWebRequest#3567399::Abort()
    System.Net Error: 0 : [2244] Exception in HttpWebRequest#3567399:: - The request was aborted: The request was canceled..
/////// THIS IS WHERE THE 30-60 SECOND PAUSE HAPPENS ////////
    System.Net Error: 0 : [5456] Can't retrieve proxy settings for Uri 'https://www.example.com/file.txt'. Error code: 12180.
    System.Net Verbose: 0 : [5456] ServicePoint#39086322::ServicePoint(www.example.com:443)
    System.Net Information: 0 : [5456] Associating HttpWebRequest#3567399 with ServicePoint#39086322
    System.Net Verbose: 0 : [2244] Exiting HttpWebRequest#3567399::Abort() 
    System.Net Verbose: 0 : [5456] HttpWebRequest#3567399::EndGetResponse()
    System.Net Error: 0 : [5456] Exception in HttpWebRequest#3567399::EndGetResponse - The request was aborted: The request was canceled..
    System.Net Verbose: 0 : [5456] Exiting HttpWebRequest#3567399::BeginGetResponse()   -> ContextAwareResult#36181605

So, it looks like the HttpClient() object is getting disposed? But how? To verify that this may be the case, I changed the code to this:

private static HttpClient _hc;
public async static Task<RemoteFileDetails> GetRemoteFileDetailsAsync(string strUrl, double dblTimeoutSeconds = 15.0)
{
    _hc = new HttpClient();
    try
    {
        _hc.Timeout = TimeSpan.FromSeconds(dblTimeoutSeconds);
        using (var h = new HttpRequestMessage(HttpMethod.Head, strUrl))
        {
            h.Headers.UserAgent.ParseAdd(UserAgent);
            using (var rm = await _hc.SendAsync(h))
            {
                rm.EnsureSuccessStatusCode();
                return new RemoteFileDetails()
                {
                    Url = strUrl,
                    FileName = rm.Content.Headers.ContentDisposition.FileName,
                    FileSize = rm.Content.Headers.ContentLength.GetValueOrDefault(),
                    LastModified = rm.Content.Headers.LastModified.GetValueOrDefault().LocalDateTime,
                    Valid = true
                };
            }
        }
    }
    catch (Exception ex)
    {
        System.Windows.MessageBox.Show(ex.ToString());
    }
    return new RemoteFileDetails();
}

but it still deadlocks the same.

Is there any explanation is to why this is happening? I thought async/await was basically "pausing" the routine, so the HttpClient should never dispose, anyway.

Or am I missing the boat totally here and it's something else.

Here is a screencap of the exception: enter image description here

ETA: I suppose it's important to show how I am calling this. This is a WPF app using MVVM Light. In the main view model, i am calling this as so:

    public MainViewModel(IDataService ds)
    {
        _dataService = ds;
        this.Initialize();
    }

    private async void Initialize()
    {
        var det = await RemoteFileUtils.GetRemoteFileDetailsAsync("https://example.com/file.txt");
}
Andy
  • 12,859
  • 5
  • 41
  • 56
  • Have you tried `.ConfigureAwait(continueOnCapturedContext:false)`? I wonder if you call this function directly from the UI thread. – xxbbcc May 18 '17 at 14:29
  • @xxbbcc yes, it doesn't change anything. Tried that yesterday. I will update the question to show how I am calling it. – Andy May 18 '17 at 14:38
  • @xxbbcc OK, i updated the question to show how i'm calling it. – Andy May 18 '17 at 14:52
  • 2
    @Sonic: Since call this.Initialize(); is not awaited it may cause deadlock. Probably this article may be helpful. https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html – Pankaj Kapare May 18 '17 at 15:03
  • I don't think it's being disposed; the request is being cancelled. [`12180` is `ERROR_WINHTTP_AUTODETECTION_FAILED`: "unable to discover the URL of the Proxy Auto-Configuration (PAC) file"](https://msdn.microsoft.com/en-us/library/windows/desktop/aa383770(v=vs.85).aspx), so it sounds like a proxy configuration error. – Stephen Cleary May 18 '17 at 15:30

1 Answers1

2

The problem originates with how it is being called.

Do not call that in the constructor of the calling class. Also avoid async void unless it is in an event handler.

public class MainViewModel : ViewModelBase {

    public MainViewModel(IDataService ds) {
        _dataService = ds;
        this.Initialize += OnInitialize;
        //Un-comment if you want to call event immediately
        //Initialize(this, EventArgs.Empty);
    }

    public event EventHandler Initialize;

    private async void OnInitialize(object sender, EventArgs e) {
        var det = await RemoteFileUtils.GetRemoteFileDetailsAsync("https://example.com/file.txt");
    }

    //Or call this after class has created.
    public void Ready() {
        Initialize(this, EventArgs.Empty);
    }
}

Next try to not create and dispoase a new HttpClient every time you need to make a request. Create one instance and use that through out the life of the application.

keep an instance of HttpClient for the lifetime of your application for each distinct API that you connect to.

GetRemoteFileDetailsAsync should also be refactored to be an injectable service rather than being statically accessed.

Community
  • 1
  • 1
Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • One question: You say that I should have only one HttpClient per application. Can I use the HttpClient object between multiple threads at the same time? or would have i have to lock the HttpClient exclusively to the thread until the HTTP transfer is finished? – Andy May 18 '17 at 15:55
  • 1
    @Sonic check the link in the answer. No need to lock the client. it is thread safe – Nkosi May 18 '17 at 15:57
  • One more question. In the example above, you say `//Un-comment if you want to call event immediately`, but if I do that, aren't i back in the same boat where I am calling an `async void` method without using await? – Andy May 18 '17 at 15:58
  • @Sonic Calling event handler this time where async void is allowed. When you get some time read up on this article [Async/Await - Best Practices in Asynchronous Programming](https://msdn.microsoft.com/en-us/magazine/jj991977.aspx) by Stephen Cleary – Nkosi May 18 '17 at 15:59
  • 1
    @Sonic just realized my mistake. You are suppose to raise the event, which in turn will call the event handler. – Nkosi May 18 '17 at 16:02