Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SmtpClient.SendMailAsync causes deadlock when throwing a specific exception

I'm trying to set up email confirmation for an ASP.NET MVC5 website, based on the example AccountController from the VS2013 project template. I've implemented the IIdentityMessageService using SmtpClient, trying to keep it as simple as possible:

public class EmailService : IIdentityMessageService
{
    public async Task SendAsync(IdentityMessage message)
    {
        using(var client = new SmtpClient())
        {
            var mailMessage = new MailMessage("[email protected]", message.Destination, message.Subject, message.Body);
            await client.SendMailAsync(mailMessage);
        }
    }
}

The controller code that is calling it is straight from the template (extracted into a separate action since I wanted to exclude other possible causes):

public async Task<ActionResult> TestAsyncEmail()
{
    Guid userId = User.Identity.GetUserId();
    
    string code = await UserManager.GenerateEmailConfirmationTokenAsync(userId);
    var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = userId, code = code }, protocol: Request.Url.Scheme);
    await UserManager.SendEmailAsync(userId, "Confirm your account", "Please confirm your account by clicking <a href=\"" + callbackUrl + "\">here</a>");

    return View();
}

However I'm getting odd behavior when the mail fails to send, but only in one specific instance, when the host is somehow unreachable. Example config:

<system.net>
    <mailSettings>
        <smtp deliveryMethod="Network">
            <network host="unreachablehost" defaultCredentials="true" port="25" />
        </smtp>
    </mailSettings>
</system.net>

In that case, the request appears to deadlock, never returning anything to the client. If the mail fails to send for any other reason (e.g. host actively refuses connection) the exception is handled normally and I get a YSOD.

Looking at the Windows event logs, it seems that an InvalidOperationException is thrown around the same timeframe, with the message "An asynchronous module or handler completed while an asynchronous operation was still pending."; I get that same message in a YSOD if I try to catch the SmtpException in the controller and return a ViewResult in the catch block. So I figure the await-ed operation fails to complete in either case.

As far as I can tell, I am following all the async/await best practices as outlined in other posts on SO (e.g. HttpClient.GetAsync(...) never returns when using await/async), mainly "using async/await all the way up". I've also tried using ConfigureAwait(false), with no change. Since the code deadlocks only if a specific exception is thrown, I figure the general pattern is correct for most cases, but something is happening internally that makes it incorrect in that case; but since I'm pretty new to concurrent programming, I've a feeling I could be wrong.

Is there something I'm doing wrong ? I can always use a synchronous call (ie. SmtpClient.Send()) in the SendAsync method, but it feels like this should work as is.

like image 649
regexen Avatar asked Feb 04 '15 23:02

regexen


1 Answers

Try this implementation, just use client.SendMailExAsync instead of client.SendMailAsync. Let us know if it makes any difference:

public static class SendMailEx
{
    public static Task SendMailExAsync(
        this System.Net.Mail.SmtpClient @this,
        System.Net.Mail.MailMessage message,
        CancellationToken token = default(CancellationToken))
    {
        // use Task.Run to negate SynchronizationContext
        return Task.Run(() => SendMailExImplAsync(@this, message, token));
    }

    private static async Task SendMailExImplAsync(
        System.Net.Mail.SmtpClient client, 
        System.Net.Mail.MailMessage message, 
        CancellationToken token)
    {
        token.ThrowIfCancellationRequested();

        var tcs = new TaskCompletionSource<bool>();
        System.Net.Mail.SendCompletedEventHandler handler = null;
        Action unsubscribe = () => client.SendCompleted -= handler;

        handler = async (s, e) =>
        {
            unsubscribe();

            // a hack to complete the handler asynchronously
            await Task.Yield(); 

            if (e.UserState != tcs)
                tcs.TrySetException(new InvalidOperationException("Unexpected UserState"));
            else if (e.Cancelled)
                tcs.TrySetCanceled();
            else if (e.Error != null)
                tcs.TrySetException(e.Error);
            else
                tcs.TrySetResult(true);
        };

        client.SendCompleted += handler;
        try
        {
            client.SendAsync(message, tcs);
            using (token.Register(() => client.SendAsyncCancel(), useSynchronizationContext: false))
            {
                await tcs.Task;
            }
        }
        finally
        {
            unsubscribe();
        }
    }
}
like image 70
noseratio Avatar answered Oct 19 '22 14:10

noseratio