Sitecore MVC applications and the Application_Error event

20 januari 2014 om 00:00 by Ruud van Falier - Post a comment

In ASP.NET applications you can catch all application exceptions in the Application_Error event handler in the global.asax.
When you use Sitecore MVC, this event is not fired by default.
It took me a while to figure out why this is, so I decided to write a short blog post about it.

What I want to do is:

  • Catch any unhandled application exception.
  • Generate an error report.
  • Display a custom error page with the error report.

My first idea (and what I have done in previous solutions) was to add logic to the Application_Error handler:


  protected void Application_Error(object sender, EventArgs e)
  {
    HttpContext httpContext = HttpContext.Current;

    // Get the exception on which the event has been fired.
    Exception exception = httpContext.Server.GetLastError();
    var errorInfo = new StringBuilder();

    // Generate an error report.
    errorInfo.AppendLine(string.Concat("URL: ", httpContext.Request.Url));
    /* Snipped additional lines of report generation */
    errorInfo.AppendLine(string.Concat("Source: ", exception.Source));
    errorInfo.AppendLine(string.Concat("Message: ", exception.Message));
    errorInfo.AppendLine(string.Concat("Stacktrace: ", exception.StackTrace));
    
    // Store the report in a session variable so we can access it from the custom error page.
    httpContext.Session["Application.ErrorInfo"] = errorInfo.ToString();

    // Return a 500 status code and execute the custom error page.
    httpContext.Server.ClearError();
    httpContext.Response.StatusCode = 500;
    httpContext.Server.Execute("/500.aspx");
  }

The issue with this is that the Application_Error is never fired for Sitecore MVC applications.
That is because, during initialization, Sitecore adds a global MVC filter that implements the OnException event, thereby bypassing the HttpApplication.Error event.

In the <initialize> pipeline the InitializeGlobalFilters processor is added.
That is the processor that adds the PipelineBasedRequestFilter filter to the global filters which implements the OnException() method:


  // Code from Sitecore.Mvc.Pipelines.Loader.InitializeGlobalFilters (Sitcore.Mvc.dll)
  public class InitializeGlobalFilters
  {
    public virtual void Process(PipelineArgs args)
    {
      this.AddGlobalFilters(args);
    }

    protected virtual void AddGlobalFilters(PipelineArgs args)
    {
      GlobalFilters.Filters.Add(this.CreateRequestFilter());
    }
  }

  // Code from Sitecore.Mvc.Filters.PipelineBasedRequestFilter (Sitecore.Mvc.dll)
  public class PipelineBasedRequestFilter : IActionFilter, IResultFilter, IExceptionFilter
  {
    public virtual void OnException(ExceptionContext exceptionContext)
    {
      Assert.ArgumentNotNull((object) exceptionContext, "exceptionContext");
      
      using (TraceBlock.Start("Exception event"))
      {
        PipelineService.Get()
          .RunPipeline<ExceptionArgs>("mvc.exception", new ExceptionArgs(exceptionContext));
      }
    }
  }

So when the OnException() handler is fired, the pipeline is executed.
By default, this pipeline contains just the Sitecore.Mvc.Pipelines.MvcEvents.Exception.ShowAspNetErrorMessage processor which will display the default ASP.NET error page (yellow screen of death).

All we have to do is create our own processor that implements the code that we would have normally put in the Application_Error method.
First of all we change the configuration so that our processor is called instead of Sitecore's default one:


  <pipelines>
    <mvc.exception>
      <processor type="Sitecore.Mvc.Pipelines.MvcEvents.Exception.ShowAspNetErrorMessage, Sitecore.Mvc">
        <patch:attribute name="type">ParTech.Pipelines.HandleMvcException, ParTech</patch:attribute>
      </processor>
    </mvc.exception>
  </pipelines>

And then we create a class for our processor:


    public class HandleMvcException : ExceptionProcessor
    {
        public override void Process(ExceptionArgs args)
        {
            var context = args.ExceptionContext;
            var httpContext = context.HttpContext;
            var exception = context.Exception;

            if (context.ExceptionHandled || httpContext == null || exception == null)
            {
                return;
            }

            // Create a report with exception details.
            string exceptionInfo = this.GetExceptionInfo(httpContext, exception);

            // Store the report in a session variable so we can access it from the custom error page.
            httpContext.Session["Application.ErrorInfo"] = exceptionInfo;

            // Return a 500 status code and execute the custom error page.
            httpContext.Server.ClearError();
            httpContext.Response.StatusCode = 500;
            httpContext.Server.Execute("/500.aspx");
        }

        private string GetExceptionInfo(HttpContextBase httpContext, Exception exception)
        {
            // Generate an error report.
            var errorInfo = new StringBuilder();
            errorInfo.AppendLine(string.Concat("URL: ", httpContext.Request.Url));
      /* Snipped additional lines of report generation */
            errorInfo.AppendLine(string.Concat("Source: ", exception.Source));
            errorInfo.AppendLine(string.Concat("Message: ", exception.Message));
            errorInfo.AppendLine(string.Concat("Stacktrace: ", exception.StackTrace));

            return errorInfo.ToString();
        }
    }

There you have it!
It's a nice way of making sure no application exception has to go unnoticed.