Abhishek Malaviya
5 min readMay 10, 2019

--

Exception Handling In Sitecore multi-site application

In this post we will know how to handle exception on Sitecore MVC based application. Here in this article we will discuss about both ServerError(500) PageNotFound(400) exception handling at application level, where we have different layer like Feature(MVC) and Foundation.

Below code is based on habitat architecture(Helix Principle) but can use same concept in plain MVC application also.

There are two types of server exceptions:

  1. MVC exception(Sitecore override MVC exception handling, so we need to use Sitecore mvc.exception pipeline for own custom implementation)
  2. Application exception, which is not part of MVC code(In Foundation layer as per Habitat architecture) ex- exception on Service,Extension,WFFM project etc, which we need to handle at application level.

MVC exception

We need to implement mvc.exception pipeline, if any error throw at MVC rendering, request will redirect to error page which is configure at site level, Please see below code:

using SC = Sitecore;

namespace Sitecore.Foundation.ExceptionHandler
{
/// <summary>
/// Class RenderingExceptionHandler.
/// </summary>
/// <seealso cref="Sitecore.Mvc.Pipelines.MvcEvents.Exception.ExceptionProcessor" />
public class RenderingExceptionHandler : ExceptionProcessor
{
/// <summary>
/// Processes the specified arguments.
/// </summary>
/// <param name="args">The arguments.</param>
public override void Process(ExceptionArgs args)
{
if(!HandleException.CanShowErrorPage())
{
return;
}
var rootItem = SC.Context.Database.GetItem(SC.Context.Site.RootPath);
var errorPageUrl = rootItem.Fields[Constant.ExceptionPageUrl];
HandleException.LogException(rootItem, args.ExceptionContext.Exception);
args.ExceptionContext.ExceptionHandled = true;
if (errorPageUrl != null)
{
Web.WebUtil.Redirect(HandleException.GenerateRedirecturl(errorPageUrl), false);
}
}
}
}

Foundation.RenderingExceptionHandling.config: Here using this patch file we are configuring mvc.exception pipeline.

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<settings>
<setting name="EnableExceptionHandling">
<patch:attribute name="value">false</patch:attribute>
</setting>
</settings>
<pipelines>
<mvc.exception>
<processor type="Sitecore.Mvc.Pipelines.MvcEvents.Exception.ShowAspNetErrorMessage, Sitecore.Mvc">
<patch:attribute name="type">Sitecore.Foundation.ExceptionHandler.RenderingExceptionHandler, Sitecore.Foundation.ExceptionHandler</patch:attribute>
</processor>
</mvc.exception>
</pipelines>
</sitecore>
</configuration>

Application exception

Non-MVC exception we can handle using application level error handling mechanism.if any error request will redirect to error page configure at site level. We are using application_error event. Also keep in mind we need to override Global.ascx and replace into website folder.

<%@ Application Codebehind=”Global.asax.cs” Inherits=”Sitecore.Foundation.ExceptionHandler.Application” Language=”C#” %>

using System;
using System.Web;
using SC = Sitecore;

namespace Sitecore.Foundation.ExceptionHandler
{
public class Application : SC.Web.Application
{
/// <summary>
/// Handling non-mvc rendering exceptions
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
protected void Application_Error(object sender, EventArgs e)
{
Exception exception = Server.GetLastError();

if (!HandleException.CanShowErrorPage() || exception == null)
{
return;
}

HttpException httpException = exception as HttpException;
int? statusCode = httpException?.GetHttpCode();
bool IsPageNotFound = statusCode.HasValue && statusCode.Value.Equals(Constant.PageNotFound);

var rootItem = SC.Context.Database.GetItem(SC.Context.Site.RootPath);
var errorPageUrl = IsPageNotFound ? rootItem.Fields[Constant.PageNotFoundUrl] : rootItem.Fields[Constant.ExceptionPageUrl];

if (statusCode.HasValue && !statusCode.Value.Equals(Constant.PageNotFound))
{
HandleException.LogException(rootItem, exception);
}
Response.Clear();
Server.ClearError();

if (errorPageUrl != null)
{
Web.WebUtil.Redirect(HandleException.GenerateRedirecturl(errorPageUrl), false);
}
}
}
}

Page Not Found(404)

If user trying to access some page which is not exist, this type request we are handling through httpRequestBegin pipeline, and redirecting to 404 page which is configure at site level.

Please see below code.

using Sitecore.Pipelines.HttpRequest;
using System.IO;
using System.Net;
using System.Web;

namespace Sitecore.Foundation.ExceptionHandler
{
/// <summary>
/// Handle 404/PageNotFound error
/// </summary>
public class Handle404PageNotFound : HttpRequestProcessor
{
public override void Process(HttpRequestArgs args)
{
if (!HandleException.CanShowErrorPage())
{
return;
}
//Skip Api request
if (HttpContext.Current.Request.Url.Segments.Length > 1 && HttpContext.Current.Request.Url.Segments[1].Equals(Constant.ApiSegment))
{
return;
}
if (Context.Item != null || Context.Site == null)
{
return;
}
//Item is valid or file is exist
if (IsValidItem(args) || File.Exists(HttpContext.Current.Server.MapPath(args.Url.FilePath)))
{
return;
}

if (Context.Database != null)
{
var rootItem = Context.Database.GetItem(Context.Site.RootPath);
var pageNotFoundUrl = rootItem.Fields[Constant.PageNotFoundUrl];
//404 item is configured for site
if (pageNotFoundUrl != null)
{
// return 404 HTTP status code in either cases, to be picked up further ahead
HttpContext.Current.Response.TrySkipIisCustomErrors = true;
HttpContext.Current.Response.StatusCode = (int)HttpStatusCode.NotFound;
Web.WebUtil.Redirect(HandleException.GenerateRedirecturl(pageNotFoundUrl), false);
}
}
}

private bool IsValidItem(HttpRequestArgs args)
{
if (Context.Item == null || Context.Item.Versions.Count == 0 || Context.Item.Visualization.Layout == null)
{
return false;
}
return true;
}
}
}

Foundation.404PageNotFound.config: This is patch file for custom implementation of httpRequestBegin pipeline.

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<pipelines>
<httpRequestBegin>
<processor patch:before="processor[@type='Sitecore.Pipelines.HttpRequest.LayoutResolver, Sitecore.Kernel']"
type="Sitecore.Foundation.ExceptionHandler.Handle404PageNotFound, Sitecore.Foundation.ExceptionHandler"
patch:after="processor[@type='Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel']" />
</httpRequestBegin>
</pipelines>
</sitecore>
</configuration>

Common helper class — HandleException which has many common methods which we are using on many places:

  1. LogException(Data.Items.Item rootItem,Exception exception): It will log exception, where we can write our own custom logic for more details like site name,page URL,user information etc.
  2. CanShowErrorPage(): We can on/off exception handling based on config settings.
  3. IsCMSRequest() : this will validate request is coming from CD server i.e non-CMS specific request.
using System;
using System.Web;
using System.Web.Configuration;
using SC = Sitecore;

namespace Sitecore.Foundation.ExceptionHandler
{
public class HandleException
{
internal static void ExceptionRediect(Exception exception)
{
HttpException httpException = exception as HttpException;
if (!CanShowErrorPage() || exception == null)
{
return;
}
int? statusCode = httpException?.GetHttpCode();
bool IsPageNotFound = statusCode.HasValue && statusCode.Value.Equals(Constant.PageNotFound);

var rootItem = SC.Context.Database.GetItem(SC.Context.Site.RootPath);
var errorPageUrl = IsPageNotFound ? rootItem.Fields[Constant.PageNotFoundUrl] : rootItem.Fields[Constant.ExceptionPageUrl];

LogException(rootItem,exception);

if (errorPageUrl != null)
{
Web.WebUtil.Redirect(GenerateRedirecturl(errorPageUrl), false);
}
}
/// <summary>
/// Generate URL
/// </summary>
/// <param name="link"></param>
/// <returns></returns>
internal static string GenerateRedirecturl(InternalLinkField link)
{
string redirectUrl = null;
if (link != null)
{
if (link.TargetItem != null)
{
var options = new UrlOptions
{
AlwaysIncludeServerUrl = true,
LanguageEmbedding = LanguageEmbedding.Always,
LowercaseUrls = true
};
redirectUrl = LinkManager.GetItemUrl(link.TargetItem, options);
redirectUrl = StringUtil.EnsurePostfix('/', redirectUrl).ToLower();
}
}
return redirectUrl;
}
/// <summary>
/// Exception has Page Url and userId
/// </summary>
/// <param name="rootItem"></param>
/// <param name="exception"></param>
internal static void LogException(Data.Items.Item rootItem,Exception exception)
{
var exceptionSubject = $"Exception on Website:{rootItem.Name}, Page:{HttpContext.Current.Request.Url.AbsoluteUri}";
var userId = HttpContext.Current.Session == null ? null : HttpContext.Current.Session["UserID"];
if (userId!= null)
{
exceptionSubject = exceptionSubject + $", UserId:{userId.ToString()}";
}
Diagnostics.Log.Error(exceptionSubject, exception, "Error");
}
/// <summary>
/// CanShowErrorPage
/// </summary>
/// <param name="exception"></param>
/// <returns></returns>
internal static bool CanShowErrorPage()
{
CustomErrorsMode mode = SC.Configuration.Settings.CustomErrorsMode;
//Local Environment/Sitecore CMS URL then do not handle any exceptions.
if (mode == CustomErrorsMode.Off || (mode == CustomErrorsMode.RemoteOnly && HttpContext.Current.Request.IsLocal)
|| IsCMSRequest()
)
{
return false;
}
return true;
}

/// <summary>
/// Is CMS related request
/// </summary>
/// <returns></returns>
private static bool IsCMSRequest()
{
return (HttpContext.Current.Request.Url.Segments.Length>1 && HttpContext.Current.Request.Url.Segments[1].Contains(Constant.Sitecore)) // Starting request with "sitecore/"
|| Context.PageMode.IsExperienceEditor || Context.PageMode.IsDebugging || Context.PageMode.IsPreview || Context.User.IsAdministrator;
}
}

In multi site scenario where we have common source code but have different site, we need to show site specific error pages.

Last thing,now we need to setup error page at site level, what we will do add page error template section at interface template like feature template which will be inherit by all sites root template.

Then we will be added two error pages ex- 404 and 500 at each sites, so error page will be site specific.

Thanks for reading. Happy coding.

--

--

Abhishek Malaviya

AI, Cloud, DevOps, Microservice, .NET, Sitecore, JavaScript, Database