LibrarySites.Banner

Handling Rendering Exceptions in MVC Solutions with the Sitecore ASP.NET CMS

This blog post explains how you can handle exceptions generated by all types of renderings in MVC solutions using the Sitecore ASP.NET web Content Management System (CMS).

For background information on this topic, see the blog post linked in the Resources section at the bottom of this page.

My initial thinking on this topic was to replace all of the processors in the mvc.renderRendering pipeline with a single processor that would add exception management logic around the invocation of a new pipeline that would contain all of those original processors. I am certain that solution is possible, but I decided to override only the ExecuteRenderer, as I am not sure that I want to handle exceptions in those other processors (InitializeProfiling, RenderFromCache, etc.). So you should still manage exceptions at a higher level.

Here is some code to override ExecuteRenderer:

namespace Sitecore.Sharedsource.Mvc.Pipelines.Response.RenderRendering
{
  using System;
  using System.IO;
  using System.Text;
  using System.Web;
  using System.Web.Configuration;
  using System.Web.UI;
  
  using SC = Sitecore;
  
  public class ExecuteRenderer :
    SC.Mvc.Pipelines.Response.RenderRendering.ExecuteRenderer
  {
    public bool ShowExceptionsToAdministrators { get; set; }
    public bool ShowExceptionsInPageEditor { get; set; }
    public bool ShowExceptionsInPreview { get; set; }
    public bool ShowExceptionsInDebugger { get; set; }
  
    public override void Process(
      SC.Mvc.Pipelines.Response.RenderRendering.RenderRenderingArgs args)
    {
      TextWriter restoreWriter = args.Writer;
  
      try
      {
        StringBuilder sb = new StringBuilder();
  
        using (StringWriter sw = new StringWriter(sb))
        {
          // nested try attempts to workaround defect in HtmlTextWriter
          HtmlTextWriter hw = new HtmlTextWriter(sw);
  
          try
          {
            args.Writer = hw;
            base.Process(args);
          }
          finally
          {
            hw.Close();
            hw.Dispose();
          }
        }
  
        restoreWriter.Write(sb.ToString());
      }
      catch (Exception ex)
      {
        args.Cacheable = false;
        SC.Diagnostics.Log.Error(
          "Rendering exception processing " + args.Rendering + " for " + SC.Context.RawUrl,
          ex,
          this);
  
        if (this.ShouldRenderErrors())
        {
          SC.Web.UI.WebControls.ErrorControl errorControl = SC.Configuration.Factory.CreateErrorControl(
            HttpUtility.HtmlEncode("Rendering exception processing " + args.Rendering + " : " + ex.Message),
            ex.ToString());
          restoreWriter.Write(errorControl.RenderAsText());
        }
        else
        {
          // if you don't ensure proper exception handling at a higher level
          // you may prefer to redirect here.
          // SC.Web.WebUtil.RedirectToErrorPage(
          //   SC.Globalization.Translate.Text("An error occurred."));
          throw;
        }
      }
      finally
      {
        args.Writer = restoreWriter;
      }
    }
  
    protected bool ShouldRenderErrors()
    {
      CustomErrorsMode mode = SC.Configuration.Settings.CustomErrorsMode;
  
      return mode == CustomErrorsMode.Off
        || (this.ShowExceptionsToAdministrators && SC.Context.User.IsAdministrator)
        || (this.ShowExceptionsInPageEditor && SC.Context.PageMode.IsPageEditor)
        || (this.ShowExceptionsInPreview && SC.Context.PageMode.IsPreview)
        || (this.ShowExceptionsInDebugger && SC.Context.PageMode.IsDebugging)
        || (mode == CustomErrorsMode.RemoteOnly && HttpContext.Current.Request.IsLocal);
    }
  }
}

Here is a Web.config include file (Sitecore.Sharedsource.Mvc.ExecuteRendering.config in my case) to enable this override of ExecuteRenderer:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <mvc.renderRendering>
        <processor type="Sitecore.Mvc.Pipelines.Response.RenderRendering.ExecuteRenderer, Sitecore.Mvc">
          <patch:attribute name="type">Sitecore.Sharedsource.Mvc.Pipelines.Response.RenderRendering.ExecuteRenderer, Sitecore.Sharedsource.Mvc</patch:attribute>
        </processor>
      </mvc.renderRendering>
    </pipelines>
  </sitecore>
</configuration>

Because an MVC layout is a view, and Sitecore uses the mvc.renderRendering pipeline to invoke all views, this solution handles exceptions in the layout view as well as all nested renderings. Unfortunately, the ErrorControl (intended for use within a page) does not generate <html> and <body> elements, but the browser still renders the error message correctly:

Screen capture of trapped exception at layout view level

I think it might be possible to use this fact to enable caching of entire pages as opposed to individual renderings.

If you trap exceptions at more than one level, you should probably implement a class to encapsulate some of the logic I've replicated in multiple classes in these blog posts.

Resources:

 

  • John,   Great article as always.  It's been a while since this bug in htmltextwriter was identified. I should imagine that Microsoft would have fixed this by now? Do you know if this has been identified as a Microsoft bug?  - Lars

  • Hi Lars,  Actually, I circulated variants of this code past numerous Sitecore staff before posting and could not get a definitive answer. While I expect Microsoft would have addressed the issue by now, one of the most technical people that responded to me said they couldn’t find any difference between .NET 2.0 and 4.0 (maybe it only affected 1.x?).  Anyway I have a feeling that this issue may only affect sinks that are streams, not objects in memory (what’s the point of buffering data written to memory, and there doesn’t seem to be anything to dispose anyway). But I thought it might be best to call out the issue and be safe rather than potentially sorry. I hope to get a resolution and update the code in this and another post that uses a similar approach, but I am not sure how to get complete confirmation, especially as the issue is not specific to Sitecore.  Thanks for reading,     -John