In this post I’m going to show you the way ASP.NET (MVC) handles exceptions that occur in web applications. We will also examine different places where we can hook our own loggers. Our application will be a very basic ASP.NET MVC project with one controller and one view:
using System; using System.Web; using System.Web.Mvc; [HandleError] public class HomeController : Controller { // // GET: /Home/ public ActionResult Index() { return View(); } public ActionResult Exception() { throw new Exception("test exception"); } }
@{ ViewBag.Title = "test title"; } <h2>Index page</h2> <a href="@Url.Action("Exception")">throw exception</a>
Let’s attach windbg to the IIS Express server hosting our sample application, configure it to stop on all first chance CLR exceptions and break when HandleError filter is called:
0:030> sxe clr 0:027> !Name2EE System.Web.Mvc.dll System.Web.Mvc.HandleErrorAttribute.OnException Module: 645c1000 Assembly: System.Web.Mvc.dll Token: 060009a3 MethodDesc: 645f13d4 Name: System.Web.Mvc.HandleErrorAttribute.OnException(System.Web.Mvc.ExceptionContext) JITTED Code Address: 646c94e4 0:027> bp 646c94e4 *** WARNING: Unable to verify checksum for C:\Windows\assembly\NativeImages_v4.0.30319_32\System.Web.Mvc\1a2d7693f4ae1edffeb277679caefefe\System.Web.Mvc.ni.dll
ASP.NET MVC (HandleError)
After clicking the throw exception link windbg should notify us of the exception that was thrown. We may verify that it’s our exception and press go. We should now stop at our breakpoint. A quick look at the stack and we can find that the HandleError filter is called by ControllerActionInvoker
:
0:027> !pe Exception object: 03e03a68 Exception type: System.Exception Message: test exception InnerException: <none> StackTrace (generated): <none> StackTraceString: <none> HResult: 80131500 0:027> g Breakpoint 0 hit 0:027> !DumpStack OS Thread Id: 0x16b0 (27) Current frame: (MethodDesc 645f13d4 +0 System.Web.Mvc.HandleErrorAttribute.OnException(System.Web.Mvc.ExceptionContext)) ChildEBP RetAddr Caller, Callee 1058ec68 646ab72e (MethodDesc 645eb840 +0x6e System.Web.Mvc.ControllerActionInvoker.InvokeExceptionFilters(System.Web.Mvc.ControllerContext, System.Collections.Generic.IList`1<System.Web.Mvc.IExceptionFilter>, System.Exception)), calling clr!VSD_FixupAsmStub 1058ec94 646aae06 (MethodDesc 645eb7e8 +0x13a System.Web.Mvc.ControllerActionInvoker.InvokeAction(System.Web.Mvc.ControllerContext, System.String)) 1058ecc0 646aac1c (MethodDesc 645eb7f8 +0xb4 System.Web.Mvc.ControllerActionInvoker.InvokeActionMethodFilter(System.Web.Mvc.IActionFilter, System.Web.Mvc.ActionExecutingContext, System.Func`1<System.Web.Mvc.ActionExecutedContext>)), calling clr!IL_Rethrow 1058ed84 646ab3b5 (MethodDesc 645eb744 +0x61 System.Web.Mvc.ControllerActionInvoker..ctor()), calling clr!JIT_WriteBarrierEAX 1058ed98 646b10ab (MethodDesc 645ecb44 +0x6b System.Web.Mvc.Controller.ExecuteCore()), calling 03859182 1058edc0 646ab9fc (MethodDesc 645eb97c +0x5c System.Web.Mvc.ControllerBase.Execute(System.Web.Routing.RequestContext)) 1058ede8 646abc6b (MethodDesc 645eb9b0 +0xb System.Web.Mvc.ControllerBase.System.Web.Mvc.IController.Execute(System.Web.Routing.RequestContext)) ...
As we can read from the stack trace, InvokeExceptionFilter
is called from the InvokeAction
method. Here is the decompiled code to check how the handling is defined:
FilterInfo filters = this.GetFilters(controllerContext, actionDescriptor); try { AuthorizationContext authorizationContext = this.InvokeAuthorizationFilters(controllerContext, filters.AuthorizationFilters, actionDescriptor); if (authorizationContext.Result != null) { this.InvokeActionResult(controllerContext, authorizationContext.Result); } else { if (controllerContext.Controller.ValidateRequest) { ControllerActionInvoker.ValidateRequest(controllerContext); } IDictionary parameterValues = this.GetParameterValues(controllerContext, actionDescriptor); ActionExecutedContext actionExecutedContext = this.InvokeActionMethodWithFilters(controllerContext, filters.ActionFilters, actionDescriptor, parameterValues); this.InvokeActionResultWithFilters(controllerContext, filters.ResultFilters, actionExecutedContext.Result); } } catch (ThreadAbortException) { throw; } catch (Exception exception) { ExceptionContext exceptionContext = this.InvokeExceptionFilters(controllerContext, filters.ExceptionFilters, exception); if (!exceptionContext.ExceptionHandled) { throw; } this.InvokeActionResult(controllerContext, exceptionContext.Result); } return true;
The default HandleErrorAttribute
swallows exceptions inherit from the exception type specified in its constructor (by default it’s System.Exception
so all exceptions are included). After setting the exceptionContext.ExceptionHandled
to true, the ControllerActionInvoker happily continues using ActionResult
provided by the HandleErrorAttribute
(by default it’s an action of rendering the Error.cshtml view). This also means that no other ASP.NET components are notified about the problem that occured. You may ask what other components am I talking about? We can find out by examining the stack. .NET exception handling is based on SEH (Structure Exception Handling). I won’t go too deep into this matter as it’s quite complicated and differs among architectures (x64, x86), but what’s important for us is that by checking the stack from top to bottom we are able to identify all exception handles (catch blocks) waiting for exceptions that happen “above them”. Imagine that our HandleErrorAttribute
hasn’t caught the exception. In that case the framework starts looking for another catch block which will be able to handle the exception. We can emulate this behavior using !EHInfo command from the SOS extension on all method descriptors found in the stack trace presented at the beginning of this post (I “dotted” methods that do not have any catch clauses defined):
0:027> !EHInfo 645eb7e8 MethodDesc: 645eb7e8 Method Name: System.Web.Mvc.ControllerActionInvoker.InvokeAction(System.Web.Mvc.ControllerContext, System.String) Class: 645dd248 MethodTable: 646f1b1c mdToken: 06000269 Module: 645c1000 IsJitted: yes CodeAddr: 646aaccc Transparency: Transparent EHHandler 0: TYPED Clause: [646aad4b, 646aadeb] [7f, 11f] Handler: [646aadeb, 646aadf0] [11f, 124] EHHandler 1: TYPED Clause: [646aad4b, 646aadeb] [7f, 11f] Handler: [646aadf0, 646aae44] [124, 178] ... 0:027> !EHInfo 6511b544 MethodDesc: 6511b544 Method Name: System.Web.HttpApplication+CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() Class: 65102264 MethodTable: 6533f124 mdToken: 06002441 Module: 650e1000 IsJitted: yes CodeAddr: 652bdc10 Transparency: Safe critical EHHandler 0: TYPED Clause: [652bde25, 652bde3f] [215, 22f] Handler: [652bdd07, 652bdd3e] [f7, 12e] EHHandler 1: FINALLY Clause: [652bde3f, 652bde74] [22f, 264] Handler: [652bdd3e, 652bdd5c] [12e, 14c] EHHandler 2: FINALLY Clause: [652bdd5c, 652bdd7b] [14c, 16b] Handler: [652bdd7b, 652bde14] [16b, 204] ... 0:027> !EHInfo 65113f2c MethodDesc: 65113f2c Method Name: System.Web.HttpApplication.ExecuteStep(IExecutionStep, Boolean ByRef) Class: 65100b00 MethodTable: 6533721c mdToken: 060023e0 Module: 650e1000 IsJitted: yes CodeAddr: 65298a50 Transparency: Safe critical EHHandler 0: FINALLY Clause: [65298a8d, 65298aab] [3d, 5b] Handler: [65298aab, 65298acb] [5b, 7b] EHHandler 1: TYPED Clause: [65298a73, 65298b08] [23, b8] Handler: [65298b08, 65298b8e] [b8, 13e] EHHandler 2: TYPED Clause: [65298a73, 65298b08] [23, b8] Handler: [65298b8e, 65298b98] [13e, 148] EHHandler 3: TYPED Clause: [65298a73, 65298b98] [23, 148] Handler: [65298b98, 65298c39] [148, 1e9] ... 0:027> !EHInfo 6510de78 MethodDesc: 6510de78 Method Name: System.Web.HttpRuntime.ProcessRequestNotificationPrivate(System.Web.Hosting.IIS7WorkerRequest, System.Web.HttpContext) Class: 650ff9d8 MethodTable: 6533220c mdToken: 0600294b Module: 650e1000 IsJitted: yes CodeAddr: 65294d70 Transparency: Safe critical EHHandler 0: TYPED Clause: [65294e45, 65294e52] [d5, e2] Handler: [65294e52, 65294e74] [e2, 104] EHHandler 1: TYPED Clause: [65294d95, 65294f9d] [25, 22d] Handler: [65294f9d, 65294ff5] [22d, 285] ... 0:027> !EHInfo 6510fac8 MethodDesc: 6510fac8 Method Name: System.Web.Hosting.PipelineRuntime.ProcessRequestNotification(IntPtr, IntPtr, IntPtr, Int32) Class: 650e15d0 MethodTable: 65332ca8 mdToken: 06002169 Module: 650e1000 IsJitted: yes CodeAddr: 6529aaa0 Transparency: Safe critical EHHandler 0: TYPED Clause: [6529aab4, 6529aac1] [14, 21] Handler: [6529aac1, 6529aad8] [21, 38]
ASP.NET (http modules, customerrors)
We’ve already checked the first method from the above list. Now it’s time for: System.Web.HttpApplication+CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
. This method posts a request to the handler and rethrows the exception if any occurs. Next on the list is System.Web.HttpApplication.ExecuteStep(IExecutionStep, Boolean ByRef)
. After decompiling we can see that it handles any exception gracefully, sending it as a result to the caller:
internal Exception ExecuteStep(HttpApplication.IExecutionStep step, ref bool completedSynchronously) { Exception result = null; ... try { ... step.Execute(); ... } catch (Exception ex) { result = ex; ... } catch { } .... return result; }
Now, the caller which happens to be System.Web.HttpApplication+PipelineStepManager.ResumeSteps(System.Exception)
internal override void ResumeSteps(Exception error) { ... while (true) { if (syncContext.Error != null) { error = syncContext.Error; syncContext.ClearError(); } if (error != null) { this._application.RecordError(error); error = null; } if (!this._validateInputCalled || !this._validatePathCalled) { error = this.ValidateHelper(context); if (error != null) { continue; } } if (syncContext.PendingCompletion(this._resumeStepsWaitCallback)) { break; } ... error = this._application.ExecuteStep(nextEvent, ref flag4); ... } ... }
checks if an error occured while processing the request (any of the executed steps returns something different than null). If so, the PipelineStepManager
records it and notifies all error event handlers (this._application.RecordError(error)
), including http modules which subscribed to this type of events (httpContext.Error += new EventHandler(httpModule_Error)
). If event handlers do not clear the error information in Http context (httpContext.Server.ClearError()
), the error will be eventually handled by HttpRuntime
(more exactly System.Web.HttpContext.ReportRuntimeErrorIfExists(System.Web.RequestNotificationStatus ByRef)
). HttpRuntime
creates an ASP.NET Health Monitoring event (by default it logs the exception to the Application event log) and prepares a Yellow Screen Of Death, taking into consideration customErrors settings from the web.config file.
I hope after having read this post you have a better understanding of the exception flow in ASP.NET and, next time if Elmah does not report any exceptions in ASP.NET MVC application you will know where to look for the fault 🙂