Home
Atom Feed

Custom Error Pages in EPiServer

Recently, we got a request from a customer to create custom error pages for their EPiServer website. Their requirements were that they should be able to set different error pages for different areas of the website and be able to edit the error page (meaning heading and rich bodytext).

This may sound straightforward, but it did prove to be a little tricky. EPiServer has it's own error page handling, and IIS has two different elements for custom error handling (<customErrors /> and <httpErrors />). In addition, ASP.NET performs a 302 redirect to the error pages specified in "<customErrors />" which is not good for SEO.

I.e: If someone links to a page that does not exist on your site, Google would follow that link. If that link then turns into a 302 temporary redirect, Google would put that page (which does not exist) into it's index. And you don't want that. It doesn't matter if it ends up with a 404 status code at the last page, Google still indexes that first page.

With that in mind, you have at least two ways of rolling your own custom error handling. You can either do everything yourself by hooking into the Application_Error event in global.asax, or try to configure the IIS error handling. In this blog post I will focus on how to do the latter.

First off, you need to disable the EPiServer error handling. You do that by opening your episerver.config and setting the "globalErrorHandling" attribute on the "<siteSettings />" element to "Off".

Then you open web.config and find the "<customErrors />" element. Ensure that the "mode" attribute to "RemoteOnly". (This is the default mode)

Now you need to create an error handler that will be the target of all errors that happen on your site. Create a page named "ErrorHandler.aspx" inside "/Templates/ErrorPages" and make it derive from EPiServer.TemplatePage. Add the following code to the class:

/// <summary>
/// Handles the Init event of the Page control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
protected void Page_Init(object sender, EventArgs e)
{
    var statusCode = this.GetStatusCode(Request.QueryString);
    var originalPage = this.GetOriginalPage(Request.QueryString);

    if (statusCode != 0)
    {
        var errorPage = this.GetErrorPage(originalPage, statusCode);

        if (errorPage != null)
        {
            CurrentPage = errorPage;
        }
    }
}

/// <summary>
/// Gets the HTTP status code from the query string.
/// </summary>
/// <param name="queryString">The query string.</param>
/// <returns>The HTTP status code</returns>
private int GetStatusCode(NameValueCollection queryString)
{
    var requestQueryStrings = string.Empty;

    // Try to get query string value in two different ways
    if (queryString.GetKey(0) != null)
    {
        requestQueryStrings = queryString.GetKey(0);
    }
    else if (queryString[0] != null)
    {
        requestQueryStrings = queryString[0];
    }

    if (requestQueryStrings.Split(';').Length > 0)
    {
        int code;

        if (int.TryParse(requestQueryStrings.Split(';')[0], out code))
        {
            return code;
        }
    }

    return 0;
}

/// <summary>
/// Gets the original page where the error occured.
/// </summary>
/// <param name="queryString">The query string.</param>
/// <returns>The originating page</returns>
private PageData GetOriginalPage(NameValueCollection queryString)
{
    int pageId;

    if (int.TryParse(queryString[0], out pageId)) 
    {
        return this.GetPage(new PageReference(pageId));
    }

    return CurrentPage;
}

/// <summary>
/// Gets the correct error page for the specified HTTP status.
/// </summary>
/// <param name="originalPage">The original page.</param>
/// <param name="statusCode">The status code.</param>
/// <returns>The error page</returns>
private PageData GetErrorPage(PageData originalPage, int statusCode)
{
    switch (statusCode)
    {
        case 404:
            Response.StatusCode = 404;
            Response.StatusDescription = "Not Found";
            return this.GetPageFromProperty(originalPage, "ErrorPage404");
        case 500:
            Response.StatusCode = 500;
            Response.StatusDescription = "Internal Server Error";
            return this.GetPageFromProperty(originalPage, "ErrorPage500");
        default:
            Response.StatusCode = 500;
            Response.StatusDescription = "Internal Server Error";
            return this.GetPageFromProperty(originalPage, "ErrorPage500");
    }
}

/// <summary>
/// Gets an error page from a page property.
/// </summary>
/// <param name="page">The page.</param>
/// <param name="propertyName">Name of the property.</param>
/// <returns>The error page</returns>
private PageData GetPageFromProperty(PageData page, string propertyName)
{
    var errorPageRef = page[propertyName] as PageReference;

    return !PageReference.IsNullOrEmpty(errorPageRef) ? GetPage(errorPageRef) : null;
}

If you take a closer look, you'll see that I try to get the error code in two different ways. That is because the 500 error query string and the 404 query strings are a little bit different. There are probably smarter ways to do this, but it solved the problem for me. The strings "ErrorPage500" and "ErrorPage404" are references to Dynamic Properties which we will configure later.

Now it's time to set the front end of the ErrorHandler page. This will decide how your error pages will look. I have a very simple page that only shows a heading and some text:

<%@ Page Language="C#" AutoEventWireup="true" ErrorPage="~/Error.htm" CodeBehind="ErrorHandler.aspx.cs" Inherits="EyeCatch.EpiServer.CustomErrorPage.Templates.ErrorPages.ErrorHandler" %>
<asp:Content ID="Content1" ContentPlaceHolderID="plContent" runat="server">
    <h1><EPiServer:Property ID="Property1" PropertyName="Heading" runat="server" /></h1>
    <EPiServer:Property ID="Property2" PropertyName="MainBody" runat="server" />
</asp:Content>

EDIT: Added ErrorPage property to the @Page directive, after tip from Tom Pipe in the comments below.

NOTE: The only thing you need to copy from the above sample is the "ErrorPage" property in the @Page directive. This is a pointer to a static HTML file that will act as a fallback if EPiServer has problems. The rest of the content of your ErrorHandler.aspx file you can customize yourself.

The ErrorHandler page we've just created however, will never be tied to a Page Type or template. For that we create two new template pages named "ErrorPage.aspx" and "NotFoundPage.aspx".

Add the above HTML to those pages as well (Don't copy the codebehind and the @Page directive, this should be left as it is when you create the file).

NOTE: The above step is not needed for this to work, but it enables editors to see what the resulting error page looks like. The only thing you need is for 404 and 500 page types to have the properties defined in ErrorHandler.aspx

Now go to EPiServer and create a Page Type that look like this:

 

Properties:

Do the same for the Not Found page, only exchange "ErrorPage" with "NotFoundPage".

Now you need to add some dynamic properties. Click on the "Dynamic Properties" link and then press "Add Property". Create a property that looks like this:

Do the same for the 500 Error Page, just change "ErrorPage404" to "ErrorPage500" and "404 Error Page" to "500 Error Page".

With that done we can go on to create the actual error pages and set the dynamic properties:

  1. Create two pages of type "[Web] ErrorPage" and "[Web] NotFoundPage" somewhere in your site tree. (I created them right under the root node). Fill in the heading and body text (you can also insert an image if you like). Publish the pages.
  2. Click your root node and then click on the "Dynamic Properties" icon on the toolbar. (The one with 3 blocks on it)
  3. Set the "404 Not Found Page" and "500 Error Page" properties to the pages you just created. Click "Save".

Now it's time to return to web.config and do the last configuration. You can also perform this step from within IIS Manager, but I'm going to show the web.config way. (The result is the same).

Insert the following block at the end of the "<system.webServer />" block:

<httpErrors errorMode="Custom" existingResponse="Replace">
    <remove statusCode="404" subStatusCode="-1" />
    <remove statusCode="500" subStatusCode="-1" />
    <error statusCode="404" prefixLanguageFilePath="" path="/Templates/ErrorPages/ErrorHandler.aspx" responseMode="ExecuteURL" />
    <error statusCode="500" prefixLanguageFilePath="" path="/Templates/ErrorPages/ErrorHandler.aspx" responseMode="ExecuteURL" />
</httpErrors>

And you're done!

If you didn't understand what you just did, here is a walkthrough:

  1. An error occurs on your site (404 Not Found or 500 Internal Server Error).
  2. Since EPiServer is not doing any error handling, the event bubbles up to IIS.
  3. IIS looks at the <httpErrors /> element and executes the URL that is specified for the HTTP status code.
  4. It loads "ErrorHandler.aspx" directly (no redirects).
  5. During the initialization of the ErrorHandler page, it checks what kind of error occured (404 or 500).
  6. It then gets the page referenced in the Dynamic Property and loads the PageData for that page (Heading and BodyText).
  7. ErrorHandler then inserts the Heading and the BodyText into the corresponding elements in the ErrorHandler designer.
  8. The page is then loaded with your chosen text and with no ugly redirects.

 

I hope this tutorial is of use to you. If you have any questions or comments, don't hestitate to use the comment form below.