Wednesday, January 19, 2011

Dynamic error pages in EPiServer with static fallback

Recently, I got a request from a customer wanting a dynamic 404 error page. I told them I’d prefer them to stick to the static 404 error page they already had, as dynamic error pages can cause a lot of trouble. It’s recommended that a 404 page should not read from the EPiServer database or retrieve any graphics. This is due to the fact that if your 404 error page is an EPiServer page and someone deletes it on accident, you can end up in an infinite 404 loop. And we don’t want that, do we?

So what would be perfect was a dynamic error page with a static fallback! I started looking around at the plenty blog posts out there by other EPiServer developers and found some quite clever solutions. However, none of them worked quite the way I wanted it to. So then I did what I always do: Tweet about it!

The response was brilliant! A lot of people recommended the 404Handler on EPiCode, but this reads from the lang files and the customer wanted to edit the 404 page from edit mode. So I ruled that one out. Another solution, by Steve Celius, was to use the 404Handler on EPiCode and create a page that saves content to a lang file. This is in fact a good solution, but unfortunately it was presented to me too late. I had already implemented the solution explained below.

The savior of the day was Jens Altbäck, a system architect as SPV. He sent me some code he’d used in a previous project. I’ve simplified the code a bit and made a few modifications, but remember that the great mind behind it is Jens, not me. I’m just the one who’s obsessed with the “sharing is caring” mentality and therefore have an urge to blog about it :)

The idea of it all is simple! You need four things:

- ErrorPageType (optional, but recommended): A pagetype with whatever properties you need, for example Heading and ErrorMessage. You could leave this pagetype out and use one of your existing pagetypes instead, but in order to restrict the editors it’s recommended that you create this pagetype so that you can determine where in the pagetree the page can be created and who should have access to creating the error pages. As you can create this pagetype in whatever way you want, I will not show you how to do this. In the example below, however, I’ve assumed that the pagetype has two properties: Heading and MainBody.

- ErrorHandler.aspx: Determines how the error page should be displayed. Mind that the ErrorHandler.aspx is not used for any pagetypes!

- ErrorHandler.aspx.cs: Gets and sets the correct StatusCode for the Response, determines if an error page exists for the current error. If it exists, the ErrorHandler sets its currentpage to the errorpage, or else it redirects to the static error page.

- error.htm: Static error page.

Here’s a step by step guide to how this all should be put together:

1) Create your ErrorPageType

Create your ErrorPageType with the appropriate properties. Create a page of this pagetype called ‘Page not found’ somewhere in the pagetree of your EPiServer site.

2) Add a property to your startpage

Add a property to the startpage of your site. This property should be called ‘HTTPStatusCode4xxPage’ and be of type Page. Point this property to the page you created in step 1.

3) Create the error.htm static error page.

Create a static html error page called error.htm and put it in the root folder of your website. If the dynamic error page is missing, the user will be directed to this static error page instead.

4) Create the ErrorHandler

The codebehind should look like this:

public partial class ErrorHandler : Templates.Base.TemplateBase

{

  private int statusCode = 0;

  private PageData errorPage = null;

 

  protected void Page_Init(object sender, EventArgs e)

  {

    GetStatusCode();

    SetStatusCode();

    SetErrorPage();

    SetCurrentPage();

    RedirectToDefaultErrorPage();

  }

 

  private void GetStatusCode()

  {

    // More to come...

  }

 

  private void SetStatusCode()

  {

    // More to come...

  }

 

  private void SetErrorPage()

  {

    // More to come...

  }

 

  private PageData GetHTTPStatusCodePage(string errorCodeProperty)

  {

    // More to come...

  }

 

  private void SetCurrentPage()

  {

    // More to come...

  }

 

  private void RedirectToDefaultErrorPage()

  {

    // More to come...

  }

}

Let’s start by looking at the two first methods: GetStatusCode() and SetStatusCode(). In order for the ErrorHandler to work properly, we need to set Respone.StatusCode to the correct status code:

/// <summary>

/// Get the HTTP error status code from the querystring

/// </summary>

private void GetStatusCode()

{

  string[] requestQueryStrings = Request.QueryString[0].Split(';');

 

  if (requestQueryStrings.Length > 0)

  {

    int code = 0;

    if (int.TryParse(requestQueryStrings[0], out code))

    {

      statusCode = code;

    }

  }

}

 

/// <summary>

/// Set the status code of the response

/// </summary>

private void SetStatusCode()

{

  if (statusCode != 0)

  {

    Response.ClearHeaders();

    Response.StatusCode = statusCode;

  }

}

The next methods are the SetErrorPage() and the GetHTTPStatusCodePage() methods. SetErrorPage() uses GetHTTPStatusCodePage() to determine if an error page exists. If it does, the private PageData object  called errorpage is set. Notice that you can extend the SetErrorPage() method to handle other errors than HTTP 404 as well as long as you create a property for it on the startpage and add it to the web.config (see step 5).

private void SetErrorPage()

{

  switch (statusCode)

  {

    case 404:

      PageData current4xxErrorPage = GetHTTPStatusCodePage("HTTPStatusCode4xxPage");

      if (current4xxErrorPage != null)

      {

        errorPage = current4xxErrorPage;

      }

      break;

    default:

      break;

  }

}

 

/// <summary>

/// Get the page set by the errorPagePropertyName on the startpage

/// </summary>

/// <param name="errorCodeProperty">Name of the startpages property</param>

/// <returns>The page set by the errorPagePropertyName</returns>

private PageData GetHTTPStatusCodePage(string errorPagePropertyName)

{

  if (PageReference.StartPage == PageReference.EmptyReference)

    return null;

 

  PageData startPage = GetPage(PageReference.StartPage);

  if (startPage != null)

  {

    PageReference errorPageRef = startPage[errorPagePropertyName] as PageReference;

    if (!PageReference.IsNullOrEmpty(errorPageRef))

      return GetPage(errorPageRef);

    }

    return null;

}

Last, but not least: Add the SetCurrentPage() and RedirectToDefaultErrorPage() methods. The SetCurrentPage() method is in my opinion the piece of code that makes this all so brilliant! If we were to redirect the user to the error page instead of setting current page and the user refreshed the page, the HTTP request done would be towards the error page, not the original page the user requested. Setting the currentpage ensures that is the user refreshes the page, the request done is towards the original page:

private void SetCurrentPage()

{

  if (errorPage != null)

  {

    CurrentMasterPage.CurrentPage = errorPage;

  }

}

 

private void RedirectToDefaultErrorPage()

{

  if (errorPage == null)

  {

    Response.Redirect("~/error.htm", true);

  }

}

And now the ErrorHandler.aspx. In this example I’ve assumed that the ErrorPageType contains two properties: Heading and ErrorMessage. So in ErrorHandler.aspx I want to display these two properties:

<%@ Page Language="C#" AutoEventWireup="true" MasterPageFile="~/Templates/MasterPages/MasterPage.master" CodeBehind="ErrorHandler.aspx.cs" Inherits="ActaWeb.ErrorHandler" %>

 

<asp:Content ID="Content1" ContentPlaceHolderID="BodyRegion" runat="server">

  <EPiServer:Property PropertyName="Heading" DisplayMissingMessage="false" EnableViewState="false" runat="server" />

  <EPiServer:Property PropertyName="ErrorMessage" DisplayMissingMessage="false" EnableViewState="false" runat="server" />

</asp:Content>

You might be wondering why this code is required, will it ever be displayed to the user? Oh yes, it will! As we’ve set currentpage in the codebehind instead of redirecting to the errorpage, this is in fact the code shown to the user. So it doesn’t matter how the ErrorPageType looks, as that will never be shown to anyone visiting the site, it will only be viewed by the editors in edit mode.

5) Edit the <httperrors> section of your web.config file

The <httperrors> section of the web.config should now look like this:

<httpErrors errorMode="Custom">

  <clear />

  <error statusCode="404" prefixLanguageFilePath="" path="/ErrorHandler.aspx" responseMode="ExecuteURL" />

</httpErrors>

And that it!

I’d like to thank Jens Altbäck for showing me this great solution and letting me blog about it. If you’re wondering who this brilliant guy is, you should follow him on Twitter: @jensaltb

7 comments:

  1. A small but pretty good update. I just updated the error handler so that it also takes care of globalized sites. Then you can create error page in EPiServer on different language and map the correct error page when an error occurs.

    Just add this line to the method SetCurrentPage():
    CurrentMasterPage.LanguageBranch = errorPage.LanguageBranch;

    So the correct language is taken from the errorPage which are globalized in EPiServer. In your MasterPage add code to handle the globalized content.

    It works perfectly and is a very popular feature for the webmaster.

    ReplyDelete
  2. That's awesome, Jens! Great work! :)

    ReplyDelete
  3. This was what I where looking for, great post!
    One question though, I don´t understand the need of the SetCurrentPage() part, why not use the Errorhandler.aspx as the ErrorPageType?

    Best regards Johan

    ReplyDelete
  4. Do you need tempalte fundation for this?

    ReplyDelete
  5. That's great!
    I have one question, what is CurrentMasterPage on this method? where am I create it?
    private void SetCurrentPage()

    {

    if (errorPage != null)

    {

    CurrentMasterPage.CurrentPage = errorPage;

    }

    }

    ReplyDelete
    Replies
    1. CurrentMasterPage is meant to be a property with the same type as your MasterPage. For example:

      private MasterPage _currentMasterPage;
      public MasterPage CurrentMasterPage
      {
      get { return _currentMasterPage ?? (_currentMasterPage = GetMasterPage(Page.Master)); }
      }

      private static MasterPage GetMasterPage(object masterPage)
      {
      if (masterPage is MasterPage)
      {
      return (MasterPage)masterPage;
      }
      return GetMasterPage(((System.Web.UI.MasterPage)masterPage).Master);
      }

      So what we're actually doing here is cast the System.Web.UI.MasterPage object to your MasterPage object.

      Delete
  6. Hi,
    what happens if the user enters this type of url : www.myepiserversite.com/en-Gb/wwwwwww.aspx? Where wwwwww is nonsense (incorrect page) Episerver still displays a horrible Episerver error page.

    ReplyDelete