C# async deadlock with HttpClient when called from

2019-08-29 10:31发布

问题:

I am experiencing a deadlock issue with HttpClient calls. I am guessing the deadlock is the result of my incomplete knowledge about async programming and I am doing something wrong. I have simplified the actual code into the example below, but I have maintained the same method call sequence and structure. I will be grateful for some insides on what could be causing the deadlock and suggestions on how to overcome this issue.

I created an extension class for HttpClient, which has the following methods:

//HttpClientExtension class
public async static Task<HttpResponseMessage> PostAsyncCustom(this HttpClient client, Uri uri, HttpContent request, bool tryReauth)
{
    var originalResponse = await client.PostAsync(uri, request);
    var content = await originalResponse.Content.ReadAsStringAsync();
    if(tryReauth)
    {
        //check if session expired and login again
        var login = await App.Communication.Login();
        //throw exception with the reauth status
        if(login)
        {
            throw new AuthException(true);
        }
        else
        {
            throw new AuthException(false);
        }
    }
}

public async static Task<HttpResponseMessage> PostAsyncWithReauth(this HttpClient client, Uri uri, HttpContent request)
{
    return await PostAsyncCustom(client, uri, request, true);
}

public async static Task<HttpResponseMessage> PostAsyncWithoutReauth(this HttpClient client, Uri uri, HttpContent request)
{
    return await PostAsyncCustom(client, uri, request, false);
}

This methods are used inside my Communication class which looks something like this:

//Communication class
//HttpClient is defined in contstructor
public async Task<bool> Login()
{
    //Define Uri and Request
    var response = await client.PostAsyncWithoutReauth(uri, request);
    //Check response status and return success/failure
    return true;
}

public async Task<bool> FetchData()
{
    //Define Uri and Request
    try
    {
        var response = await client.PostAsyncWithReauth(uri, request);
    }
    catch(AuthException ae)
    {
        //if reauth was successfull
        if(ae)
        {
            var newResponse = await client.PostAsyncWithoutReauth(uri, request);
            //Check newResponse status and return success/failure
        }
    }
    return false;
}

So what happens is that when an AuthException is thrown with status = true and PostAsyncWithoutReauth will be called, what happens is that the break-point inside PostAsyncWithoutReauth will be triggered, but upon continuation the break-point inside PostAsyncCustom which is called from the PostAsyncWithoutReauth never triggers. The code will just continue to "execute" until Task timeout exception will be triggered. So that's why I think it is a deadlock issue. I have tried setting the .ConfigureAwait(false) on some of the calls, but the issue remained the same.

Update:

The FetchData method is called from the Xamarin.Forms ContentPage, where it is triggered by completed event on an Entry field:

Input.Completed += async (s, e) =>
{
    var status = await App.Communication.FetchData();
}

I would like to add that if instead the PostAsyncWithoutReauth call I replace it with return await FetchData(), this will not deadlock.

Update #2: I simplified the code, so my PostAsyncCustom now looks like that:

public async static Task<HttpResponseMessage> PostAsyncCustom(this HttpClient client, Uri uri, HttpContent request, bool tryReauth)
{
    var originalResponse = await client.PostAsync(uri, request);
    var content = await originalResponse.Content.ReadAsStringAsync();
    if(tryReauth)
    {
        return await PostAsyncCustom(client, uri, request, false);
    }
    return originalResponse;
}

What happens here is that the client.PostAsync will hang when called for the second time. I also tried to get rid of the recursion so I tested the code like that:

public async static Task<HttpResponseMessage> PostAsyncCustom(this HttpClient client, Uri uri, HttpContent request, bool tryReauth)
{
    var originalResponse = await client.PostAsync(uri, request);
    var content = await originalResponse.Content.ReadAsStringAsync();
    if(tryReauth)
    {
        var secondReponse = await client.PostAsync(uri, request);
    }
    return originalResponse;
}

This will also hang when the client.PostAsync is called for the second time.

Update #3:

The "deadlock" finally throws an exception with stacktrace, which can be seen here.