Knaģis
Knaģis

Reputation: 21515

How to get correct stack trace for exceptions in Web API 2 async actions?

I have a simple API controller method

public async Task<Models.Timesheet> GetByDate(DateTime date, string user = null)
{
    throw new InvalidOperationException();
}

Now the problem is that the exception stack trace I get either in my custom action filter or just by setting IncludeErrorDetailPolicy.Always is like this

System.InvalidOperationException: Operation is not valid due to the current state of the object.
   at System.Web.Http.Filters.ActionFilterAttribute.<CallOnActionExecutedAsync>d__1.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at System.Web.Http.Controllers.ActionFilterResult.<ExecuteAsync>d__2.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Web.Http.ApiController.<InvokeActionWithExceptionFilters>d__1.MoveNext()

It used to be much better with Web API v1. After upgrade to v2 the stack traces are pretty much unusable - it is expected that with async/await the stack traces will not be as they once used to be but in this case the whole stack trace does not include any hint at even the class that failed. Is there something that can be configured in Web API to mitigate the issue?

Upvotes: 15

Views: 12887

Answers (3)

Alon Catz
Alon Catz

Reputation: 2530

This has been fixed in WebApi 2.1 (assembly version 5.1.1)

Follow these instructions to upgrade to the latest version: https://www.nuget.org/packages/Microsoft.AspNet.WebApi

Upvotes: 7

Knaģis
Knaģis

Reputation: 21515

So after I asked the question I got the right motivation to properly dig through Web API code to find the issue. It seems that the culprit is ActionFilterAttribute.CallOnActionExecutedAsync method that does something like this (paraphrased, not the actual code):

try
{
    await previous();
}
catch(Exception ex)
{
    exception = ex;
}

var context = new HttpActionExecutedContext(actionContext, exception);
this.OnActionExecuted(context);
if (context.Response == null && context.Exception != null)
    throw context.Exception;

So it happens that the first filter actually gets the correct stack trace. But then it just goes and rethrows the exception, thus losing the original stack trace.

This made me realize that instead of deriving from ExceptionFilterAttribute, I need to derive from ActionFilterAttribute since the former are always called last. Unfortunately I also need to make sure my exception filter runs first but that is not built-in Web API (see this question for a correct solution for this).

My quick solution was to make sure all filters I have in the application (I only have a few) derive from my custom exception filter (and call base.OnActionExecuted()) thus every single filter will actually check for the exception and perform something like this:

public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
    if (actionExecutedContext.Exception == null)
        return;

    WriteLog(actionExecutedContext.Exception);

    actionExecutedContext.Response = actionExecutedContext.Request.CreateErrorResponse(
        System.Net.HttpStatusCode.InternalServerError,
        actionExecutedContext.Exception.Message,
        actionExecutedContext.Exception);

    actionExecutedContext.Exception = null;
}

With this workaround I got the correct stack traces both to the log and to the user if error details are turned on.

Upvotes: 4

Stephen Cleary
Stephen Cleary

Reputation: 457472

I believe stack traces are improved on .NET 4.5.1 when running on Windows 8.1 / Server 2012 R2.

Alternatively, I do have an "Async Diagnostics" NuGet package that you can install into your project. Then add this line:

[assembly: AsyncDiagnosticAspect]

And you can use the ToAsyncDiagnosticString extension method on the Exception type. ToAsyncDiagnosticString includes all the information from ToString and then appends a "logical stack". More documentation (and the source) on GitHub.

Note that there is no support for partial trust and this works best in Debug builds.

Upvotes: 1

Related Questions