71

When navigating to a url such as www.website.com/missing.pdf and www.website.com/-/media/missing.pdf you will either receive the IIS 404 Server Error message or a Sitecore document not found page.

enter image description here

What is a solid approach to handling these scenarios? Some requirements:

  1. Should support files and media items.
  2. Should return the proper response status codes (i.e. 404, 500).
  3. Should support the use of content items that represent the friendly page (such as a site map).

@jammykam recommended the following

Web.config settings

<!-- Enable for IIS 7+ -->
<httpErrors errorMode="DetailedLocalOnly" existingResponse="Replace">
  <remove statusCode="404" subStatusCode="-1" />
  <remove statusCode="500" subStatusCode="-1" />
  <error statusCode="404" path="/utility/404" responseMode="ExecuteURL" />
  <error statusCode="500" path="/utility/500.html" responseMode="File" />
</httpErrors> 

<!-- Disable for IIS 7+ -->
<customErrors mode="Off" />

where the 404 an 500 content items have a rendering with the following

public ActionResult Index()
{
    var errorCode = this.mappingProvider.GetCurrentItem<ErrorCode>();
    errorCode.StatusCode = Sitecore.MainUtil.GetInt(errorCode.StatusCode, 404);

    if (Sitecore.Context.PageMode.IsNormal)
    {
        Response.TrySkipIisCustomErrors = true;
        return new HttpStatusCodeResult(errorCode.StatusCode);
    }

    return View("~/Views/Developers/Common/StatusCode.cshtml", errorCode);
}

Update

After reviewing Justin's response here is what I ended up doing.

  1. Updated web.config to handle <httpErrors> with existingResponse="Auto".
  2. Updated ItemNotFoundUrl to an item in the tree such as /home/404.
  3. Updated FilterUrlExtensions to allow for files with the extension .pdf.
  4. Patch MediaRequestHandler to use Server.TransferRequest to avoid the error Error executing child request.
  5. Added a controller rendering to Sitecore to change the status code.
  6. Added a processor to handle the ItemNotFoundUrl setting, load the item, and set to the context item.

In case you are implementing and encounter an Error executing child request message then you may find this article helpful.

<setting name="RequestErrors.UseServerSideRedirect" value="true"/>
5
  • 1
    Can you tell me about the 4.? how do you patch the MediaRequestHandler?
    – Phoenix_uy
    Commented Mar 12, 2017 at 23:37
  • Please see the article linked at the bottom of my answer. Commented Mar 12, 2017 at 23:47
  • I've read the article and i don't understand the handlers part.. where are them? what consequences may i have if i comment the lines that the article says to comment?
    – Phoenix_uy
    Commented Mar 12, 2017 at 23:49
  • 1
    stackoverflow.com/a/16587573/1277533 Commented Mar 12, 2017 at 23:55
  • But it doesn't mess up with paths, urls, links? and where is that handlers section?
    – Phoenix_uy
    Commented Mar 12, 2017 at 23:58

7 Answers 7

85

The following setup may take some effort, but it will give you proper status codes, as well as "keeping" the requested URL, avoiding superfluous requests between Sitecore and IIS, and allow other processors and modules in your solution to seamlessly return a 404 page if required. It's been my experience that without the following setup, there is inconsistency in how Sitecore solutions return 404 pages and status codes. I've also found that in a completely ironic fashion, sometimes these pages can cause infinite redirect loops or undue strain on the IIS instance.

The high-level overview looks something like this:

  1. Have a 404 page item under your SiteRoot, with a rendering control that uses code to properly set a 404 status code.
  2. Have a 404 processor in the httpRequestBegin pipeline to override Sitecore's default 404 functionality, so that clients aren't redirected and the requested URL is preserved.
  3. Have a 404 processor in the mvc.getPageItem pipeline to force URL's that map to items to return a 404 when appropriate.

Do make sure to configure the ItemNotFoundURL setting to the 404 page item URL within your site.

<setting name="ItemNotFoundUrl" value="/404" />

Web.config

Set the existingResponse attribute to "Auto" to give your website control over how status codes get handled. The "Auto" mode allows you to set Response.TrySkipIisCustomErrors to true to bypass the default IIS 404 page, or anything that's been defined in the httpErrors configuration node.

<httpErrors errorMode="DetailedLocalOnly" existingResponse="Auto">
 <!--  We don't want to interact with Sitecore or any part of the site 
       that could be causing the HTTP 500 error in the first place, so  
       it's a good idea to make sure that you have a static 500 page outside   
       the purview of Sitecore or any other "dynamic systems" within the
       application pool/site instance.
  -->
 <error statusCode="500" path="/static/errors/500.html" responseMode="File"/>    
</httpErrors>

Setting the Status Code with a 404 Rendering Control

We need to either have a Controller Rendering or a View Rendering that has code to set a few different properties to allow us to properly return a 404 status code.

There are two critical properties that must be set in your rendering: one obviously being the Response.StatusCode to 404, and the other to set Response.TrySkipIisCustomErrors to true. Remember, TrySkipIisCustomErrors will cause IIS to ignore the configuration in the httpErrors configuration node since we've set the existingResponse attribute on that node to "Auto".

public ActionResult Request404Page()
{
   HttpContext.Current.Response.StatusCode = 404;
   HttpContext.Current.Response.TrySkipIisCustomErrors = true;
   HttpContext.Current.Response.StatusDescription = "404 File Not Found";

   return View();
}

Create a 404 Processor

Use a custom HttpRequestProcessor at the end of the pipeline to handle when a context item has not been resolved. This processor exists only to avoid Sitecore's default functionality of redirecting to the 404 page. Instead of redirecting the client or use a server side only redirect, the goal is to leave the URL alone and avoid causing a messy client experience. This is achieved merely by setting the Context.Item and ProcessorItem properties to your desired 404 page.

public class 404HttpRequestProcessor : HttpRequestProcessor
{
    public override void Process(HttpRequestArgs args)
    {
        if (Sitecore.Context.Item != null || Sitecore.Context.Site == null)
        {
            return;
        }

        if (string.Equals(Sitecore.Context.Site.Properties["enableCustomErrors"], "true", StringComparison.OrdinalIgnoreCase) == false)
        {
            return;
        }

        var pageNotFound = this.Get404PageItem();
        args.ProcessorItem = pageNotFound;
        Sitecore.Context.Item = pageNotFound;
    }

    protected Item Get404PageItem()
    {
        // This is largely up to how the project in general is setup.
        // My solutions are heavily Fortis dependent, but for Vanilla 
        // Sitecore setups you could just use the following setting:
        // Settings.GetSetting("ItemNotFoundUrl", "/errors/404");
        // and pull the Item object from that path

        var path = Sitecore.Context.Site.RootPath + "/" + Settings.GetSetting("ItemNotFoundUrl", "/errors/404");
        var item = Sitecore.Context.Database.GetItem(path);

        return item;
    }   
}

Patch your custom 404 processor, in this example called 404HttpRequestProcessor, at the end of the httpRequestBegin Pipeline.

<processor type="MyProject.Pipelines.HttpRequest.404HttpRequestProcessor, MyProject.Core"
                        patch:after="processor[@type='Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel']">
</processor>

Now we have the following:

  • A 404 page within sitecore that we can customize in anyway we want.
  • We avoid redirecting the client, so they don't lose their URL or break the back button.
  • There's no spaghetti setup within IIS that bounces requests back and forth between IIS and Sitecore.

But we're not quite done yet!

404'ing URL's That Map to an Item

Unfortunately, the above setup will only work if you type in a URL that does not map to an item in your content tree (e.g. "www.mywebsite.com/page-item/that/does-not-exist"). Even if you set Context.Item to null in your httpRequestBegin pipeline, Sitecore will "re-resolve" that URL in the mvc.getPageItem pipeline if that URL happens to map to an item in Sitecore. There are certain situations where you want to return 404 for a URL that technically exists, but if this scenario is not applicable to your project you don't need to include the following processor.

To 404 a URL that technically maps to a content item, we need to add a processor to the mvc.getPageItem pipeline. The code will be excruciatingly similar to the 404RequestProcessor processor we added in the httpRequestBegin pipeline, we'll just be modifying a few different "sister" properties to properly exit the mvc.getPageItem pipeline. Of course, since both processors are similar it's possible to abstract the code out and avoid duplication, but I'll leave that as an exercise to the reader.

It is assumed at this point that a previous processor in the pipeline doesn't like something about the current request, so that processor set args.Result to null.

public class 404PageItemProcessor : GetPageItemProcessor
{
    public override void Process(GetPageItemArgs args)
    {
        if (args.Result != null || Sitecore.Context.Site == null)
        {
            return;
        }

        if (string.Equals(Sitecore.Context.Site.Properties["enableCustomErrors"], "true", StringComparison.OrdinalIgnoreCase) == false)
        {
            return;
        }

        var pageNotFound = Get404PageItem();
        args.ProcessorItem = pageNotFound;
        args.Result = pageNotFound;     
        Sitecore.Context.Item = pageNotFound;
   }

   protected Item Get404PageItem()
   {
       // This is largely up to how the project in general is setup.
       // My solutions are heavily Fortis dependent, but for Vanilla 
       // Sitecore setups you could just use the following setting:
       // Settings.GetSetting("ItemNotFoundUrl", "/errors/404");
       // and pull the Item object from that path

       var path = Sitecore.Context.Site.RootPath + "/" + Settings.GetSetting("ItemNotFoundUrl", "/errors/404");
       var item = Sitecore.Context.Database.GetItem(path);

        return item;
    }

}

Please pay attention to the fact that the code above sets two properties on the GetPageItemArgs object: ProcessorItem and Result. Both of these properties must be set to the desired 404 page or Sitecore will simply ignore the property value that you set (for reasons I will not get into in this post).

Now, just add the 404PageItemProcessor to the end of the mvc.getPageItem pipeline.

<processor type="MyComp.Pipelines.GetPageItem.404PageItemProcessor, MyComp.Core" 
patch:after="processor[@type='Sitecore.Mvc.Pipelines.Response.GetPageItem.GetFromOldContext, Sitecore.Mvc']">
</processor>

Handling fatal errors and HTTP 500 codes

It's my opinion that 500 errors should simply return a static 500 page and not bother with doing anything needlessly fancy. The httpErrors configuration at the top of this post will give you sufficient handling of HTTP 500 error codes, since HTTP 500 status codes are typically thrown when something terrible has happened. The following will log any uncaught errors but still obey your configuration (while technically allowing you to override those settings as well). I've personally found it extremely helpful when attempting to debug fatal errors.

Add the following method to your Global class in your website project's Global.asax.cs file:

    protected void Application_Error(object sender, EventArgs e)
    {
        var error = Server.GetLastError();
        Server.ClearError(); 
        var httpException = error as HttpException;
        Response.StatusCode = httpException != null ? httpException.GetHttpCode() : (int)HttpStatusCode.InternalServerError;

        Sitecore.Diagnostics.Log.Fatal("Uncaught application error", error);
    }

You can find more information about the code in the Application_Error method above and the ideas behind it in various posts around StackOverflow, including this one: https://stackoverflow.com/questions/1171035/asp-net-mvc-custom-error-handling-application-error-global-asax.

13
  • 2
    One thing I discovered while implementing your example is include the .pdf in the allowed filterurlextensions for static files as described here in the accepted answer. stackoverflow.com/questions/27152784/… Commented Sep 30, 2016 at 13:16
  • 2
    Its recommend not to modify global.ascx.cs file, can the last step be added to httpRequestEnd pipeline? Commented Feb 7, 2017 at 14:20
  • @DheerajPalagiri Where is it recommended that you don't modify the Global.asax.cs file? You have to modify the file to get solr (and other providers) up running with whatever IoC/DI framework you're using. Commented Feb 7, 2017 at 14:28
  • 1
    @JustinLaster My understanding is, its better not to modify any out of the box sitecore files, this will make things easier for upgrade. GlassMapper use to do that in Global.asax.cs but now its been moved to <initialize> pipeline. Commented Feb 7, 2017 at 15:28
  • 1
    @JustinLaster Sitecore actually has their own Global.asax and in 8.2 you can't override the Application_Start method because they have made it internal (if you extend their version - which you should). Commented Mar 14, 2017 at 22:34
19

Edit: February 2020

NuGet package 2.0.0 was released exclusively for Sitecore 9.x support. Full implementation available at https://github.com/jraps20/SitecoreFriendlyErrors

Edit: November 2017

The technique described in this answer has been packaged up and added to NuGet: https://www.nuget.org/packages/Sitecore.FriendlyErrors/

~ ~ ~

Due to the popularity of this post, I decided to review the approved answer by @Justin and verify it solved all use-case's. I performed an exhaustive review and made a few tweaks to improve upon his answer. I reviewed other posts throughout the community as well and apologize for not tagging each post that helped contribute to this. It is all outlined in the repo ReadMe. It has been condensed for brevity on this answer. Each step is explained much more thoroughly in the blog post.

Note: This answer uses HttpRequestBase. The blog post outlines this usage. It can be removed and the processors can inherit from HttpRequestProcessor instead.

Add 404 Item to Site

/sitecore/content/Site1/home/404

Note: This method assumes all sites in a multisite environment contain a page at their root named 404. This page must have presentation details defined.

Update Settings

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <settings>
      <setting name="LayoutNotFoundUrl">
        <patch:attribute name="value">/404</patch:attribute>
      </setting>
      <setting name="ItemNotFoundUrl">
        <patch:attribute name="value">/404</patch:attribute>
      </setting>
    </settings>
  </sitecore>
</configuration>

Transfer Bad Requests to Friendly 404 Page

Out of the box, Sitecore will perform a 302 redirect to the 404 page, to instead immediately serve up the 404 page, the following changes are necessary.

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <settings>
      <setting name="RequestErrors.UseServerSideRedirect">
        <patch:attribute name="value">true</patch:attribute>
      </setting>
    </settings>
  </sitecore>
</configuration>

To read this value properly and perform a TransferRequest (which retains the URL), we need to replace the existing ExecuteRequest processor with the following:

public class CustomExecuteRequest : global::Sitecore.Pipelines.HttpRequest.ExecuteRequest
{
    private readonly BaseLinkManager _baseLinkManager;

    public CustomExecuteRequest(BaseSiteManager baseSiteManager, BaseItemManager baseItemManager, BaseLinkManager baseLinkManager) : base(baseSiteManager, baseItemManager)
    {
        _baseLinkManager = baseLinkManager;
    }

// Use the below constructor if you're experiencing issues with DI (can't find 'ctor' method)
/*
public CustomExecuteRequest()
{
    _baseLinkManager = ServiceLocator.ServiceProvider.GetRequiredService<BaseLinkManager>();
}
*/

    protected override void PerformRedirect(string url)
    {
        if (Context.Site == null || Context.Database == null || Context.Database.Name == "core")
        {
            _404Logger.Log.Info("Attempting to redirect url {0}, but no Context Site or DB defined (or core db redirect attempted)".Fmt(url));
            return;
        }

        // need to retrieve not found item to account for sites utilizing virtualFolder attribute
        var notFoundItem = Context.Database.GetItem(Context.Site.StartPath + Settings.ItemNotFoundUrl);

        if (notFoundItem == null)
        {
            _404Logger.Log.Info("No 404 item found on site: {0}".Fmt(Context.Site.Name));
            return;
        }

        var notFoundUrl = _baseLinkManager.GetItemUrl(notFoundItem);

        if (string.IsNullOrWhiteSpace(notFoundUrl))
        {
            _404Logger.Log.Info("Found 404 item for site, but no URL returned: {0}".Fmt(Context.Site.Name));
            return;
        }

        _404Logger.Log.Info("Redirecting to {0}".Fmt(notFoundUrl));
        if (Settings.RequestErrors.UseServerSideRedirect)
        {
            HttpContext.Current.Server.TransferRequest(notFoundUrl);
        }
        else
            WebUtil.Redirect(notFoundUrl, false);
    }
}

The patch:

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <httpRequestBegin>
        <!-- Reads updated "RequestErrors.UseServerSideRedirect" value and transfers request to LayoutNoutFoundUrl or ItemNotFoundUrl, preserving requested URL -->
        <processor type="MyDll.CustomExecuteRequest, MyDll" resolve="true" patch:instead="*[@type='Sitecore.Pipelines.HttpRequest.ExecuteRequest, Sitecore.Kernel']"/>
      </httpRequestBegin>
    </pipelines>
  </sitecore>
</configuration>

Return Proper HTTP Status Code

Next, the status code must be updated from 200 to 404. This technique relies on a new processor added to the httpRequestEnd pipeline after EndDiagnostics, which, while it adds minimal overhead to all requests, it avoids setting the status code via a component, which IMHO feels tacky.

public class Set404StatusCode : HttpRequestBase
{
    protected override void Execute(HttpRequestArgs args)
    {
        // retain 500 response if previously set
        if (HttpContext.Current.Response.StatusCode >= 500 || args.Context.Request.RawUrl == "/")
            return;

        // return if request does not end with value set in ItemNotFoundUrl, i.e. successful page
        if (!args.Context.Request.Url.LocalPath.EndsWith(Settings.ItemNotFoundUrl, StringComparison.InvariantCultureIgnoreCase))
            return;

        _404Logger.Log.Warn("Page Not Found: " + args.Context.Request.RawUrl + ", current status: " + HttpContext.Current.Response.StatusCode);
        HttpContext.Current.Response.TrySkipIisCustomErrors = true;
        HttpContext.Current.Response.StatusCode = (int)HttpStatusCode.NotFound;
        HttpContext.Current.Response.StatusDescription = "Page not found";
    }
}

The patch:

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <httpRequestEnd>
        <!-- Sets a 404 status code on the response -->
        <processor type="MyDll.Set404StatusCode, MyDll" patch:after="*[@type='Sitecore.Pipelines.HttpRequest.EndDiagnostics, Sitecore.Kernel']">
          <disallowedDatabases>core</disallowedDatabases>
          <disallowedSites>shell</disallowedSites>
        </processor>
      </httpRequestEnd>
    </pipelines>
  </sitecore>
</configuration>

Log URLs to Fine-Tune Detection

We find it extremely useful to log any requests that are returned as 404 to ensure we aren't hindering any out-of-the-box Sitecore functionality.

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <log4net>
      <appender name="404Appender" type="log4net.Appender.SitecoreLogFileAppender, Sitecore.Logging">
        <file value="$(dataFolder)/logs/404.log.{date}.txt"/>
        <appendToFile value="true"/>
        <layout type="log4net.Layout.PatternLayout">
          <conversionPattern value="%4t %d{ABSOLUTE} %-5p %m%n"/>
        </layout>
        <encoding value="utf-8"/>
      </appender>
      <logger name="CustomErrors._404Logger" additivity="false">
        <level value="INFO"/>
        <appender-ref ref="404Appender"/>
      </logger>
    </log4net>
  </sitecore>
</configuration>

The code:

public static class _404Logger
{
    public static ILog Log => LogManager.GetLogger("CustomErrors._404Logger") ?? LoggerFactory.GetLogger(typeof(_404Logger));
}

Implementation:

...
        _404Logger.Log.Warn("Page Not Found: " + args.Context.Request.RawUrl);
        HttpContext.Current.Response.TrySkipIisCustomErrors = true;
        HttpContext.Current.Response.StatusCode = (int)HttpStatusCode.NotFound;
        HttpContext.Current.Response.StatusDescription = "Page not found";
    }
...

Failed Media Requests Return Friendly 404 Error Page

To ensure media requests return a proper 404 (mostly used for PDF's, but applicable to all), we need to modify the base web.config. It is recommended this approach be implemented via a Config Transformation to preserve the native web.config.

public class MediaRequestHandler : global::Sitecore.Resources.Media.MediaRequestHandler
{
    protected override bool DoProcessRequest(HttpContext context)
    {
        Assert.ArgumentNotNull(context, "context");

        var request = MediaManager.ParseMediaRequest(context.Request);

        if (request == null)
            return false;

        var media = MediaManager.GetMedia(request.MediaUri);

        if (media != null)
            return DoProcessRequest(context, request, media);

        using (new SecurityDisabler())
            media = MediaManager.GetMedia(request.MediaUri);

        string str;

        if (media == null)
        {
            str = Settings.ItemNotFoundUrl;
        }
        else
        {
            Assert.IsNotNull(Context.Site, "site");
            str = Context.Site.LoginPage != string.Empty ? Context.Site.LoginPage : Settings.NoAccessUrl;
        }
        if (Settings.RequestErrors.UseServerSideRedirect)
            HttpContext.Current.Server.TransferRequest(str);
        else
            HttpContext.Current.Response.Redirect(str);
        return true;
    }
}

The web.config patch:

<!--
      Commented original MediaRequestHandler to account for 404
      <add verb="*" path="sitecore_media.ashx" type="Sitecore.Resources.Media.MediaRequestHandler, Sitecore.Kernel" name="Sitecore.MediaRequestHandler"/>-->
      <add verb="*" path="sitecore_media.ashx" type="MyDll.MediaRequestHandler, MyDll" name="Sitecore.MediaRequestHandler"/>

Handling Requests for Improper Extensions

To handle completely arbitrary requests (e.g. http://mysite.com/asdf.asdf.asdf) in a friendly way, we need to add a static 404.html page to our web root. IIS will take care of setting the status code to 404. This fringe-case simply needs to output standard 404 markup to alert the user that the page is not found. It is also patched in the web.config, shown below via a config transformation.

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <system.webServer>
    <httpErrors errorMode="DetailedLocalOnly" existingResponse="Auto" xdt:Transform="Insert" >
      <remove statusCode="404" subStatusCode="-1" />
      <error statusCode="404" path="404.html" responseMode="File" />
    </httpErrors>
  </system.webServer>
</configuration>

500 Errors

To handle 500 errors, aka Yellow Screens of Death, the following is added to the web.config, again shown as a config transformation.

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <system.web>
    <customErrors mode="RemoteOnly" defaultRedirect="500.aspx" redirectMode="ResponseRewrite" xdt:Transform="SetAttributes">
      <error statusCode="500" redirect="500.aspx" xdt:Transform="Insert" />
    </customErrors>
  </system.web>
</configuration>

The 500.aspx page added to the site root contains the following markup at the top of the file:

<% 
    Response.StatusCode = 500;
    Response.TrySkipIisCustomErrors = true;
 %>

<!doctype html>

This ensures the response sent to the browser is 500.

Note: This is not ideal for a multi-site environment where the 500 page should be site-specific. To account for this, add logic to the 500.aspx page to transfer requests appropriately given the hostName requested.


In our experience, this combination satisfies all SEO requirements. It can be packaged into a library and added to any existing implementation for easier implementation.

Note, all patches defined in this answer can be combined into a single patch config, with the exception of the web.config changes.

4
  • 1
    Edited answer to utilize DI to resolve obsolete warning in the compiler. Updated CustomExecuteRequest to respect virtualFolder and other site settings by pulling 404 link using LinkManager. Fixes multilingual issue.
    – jrap
    Commented Sep 25, 2017 at 19:56
  • 1
    A couple of thing... Your module was pretty perfect. I was able to include the error statuscode 500 in the HttpErrors, as provided in the number 1 answer. Having this, I was able to use sitecore content items to fully content control (and provide multilingual support) to both 404, 404.wrongext and 500 errors. Having a content item of 404 and a content item of error and then setting path of the error status code to the error page.
    – Nona Drake
    Commented May 23, 2019 at 5:25
  • @jrap You have a 404 handler using a static 400.html page, I'm wondering why you used that rather than <error statusCode="404" path="/Error/404" responseMode="ExecuteURL" />, as the static file creates branding issues in a multisite setup. In particular - I'm wondering if there a scenario executeUrl causes problems Commented Jul 19, 2021 at 11:06
  • Good point, you likely could point it to the content-friendly 404 instead. I don't have any further details as I wrote this such a long time ago :D
    – jrap
    Commented Jul 19, 2021 at 14:32
13

For 404 page, for not found files or items, Sitecore has out-of-box setting -

<setting name="RequestErrors.UseServerSideRedirect" value="true" />

Make sure, you define your physical 404 page in below setting -

<setting name="ItemNotFoundUrl" value="/sitecore/service/notfound.aspx" />

If you have the 404 in Sitecore content tree, you can refer blog - http://sitecoreblog.patelyogesh.in/2013/06/handle-404-page-not-found-in-sitecore.html

For 500, you can do below configurations in web.config file -

<customErrors mode="RemoteOnly" redirectMode="ResponseRewrite" defaultRedirect="~/500.aspx">
  <error statusCode="404" redirect="~/404.aspx"/>
  <error statusCode="500" redirect="~/500.aspx"/>
</customErrors>
7
  • Does this approach both ensure the status code remains as a 404/500? Commented Sep 29, 2016 at 17:45
  • 1
    See, we have multiple approaches to achieve 404 as mentioned above. Now, for 500 is raised from .net, surely above approach will be helpful. Remember that, if 500+ is coming from Load Balancer/Proxy, there is no way to achieve it. Commented Sep 29, 2016 at 17:46
  • This doesn't handle a multi-site instance tho
    – Richard Seal
    Commented Sep 29, 2016 at 18:02
  • 2
    This is a pretty solid approach for handling multisite solutions along with the IIS redirects metioned above laubplusco.net/handling-404-sitecore-avoid-302-redirects Commented Sep 29, 2016 at 18:07
  • 2
    The Link I shared - sitecoreblog.patelyogesh.in/2013/06/… can be used to handle 404 in multisite. In Sitecore, it's possible to achieve it plenty of ways, so it will be better to understand how the http request pipelines works, and implement approach that suits our requirement. Commented Sep 29, 2016 at 18:09
9

You can, but not by just putting the links in the settings. I'm afraid it's a bit more work but luckily it has been done before and shared. One example can be found in this blog by Anders Laub but there are many other examples out there - although it always comes down to the same principle:

When an item is not found by the ItemResolver or if the current item does not have a layout then we would like the response to have status code 404 and we want to show a nice looking page which the editors can control for each site in a solution.

To achieve this, you need to implement a HttpRequestProcessor and place it in the HttpRequestBegin pipeline after the ItemResolver. In the processor you can check your context (item, ...) - for performance it is important to exit as soon as possible. If you detect an "error" you can set the context item to a desired one showing the error.

4

The basic idea is that you need to create 2 different pipelines so that you can "catch" the bad page, and then after Sitecore has rendered your custom error page, you need to tell Sitecore to return a 404 code instead of whatever other code Sitecore has decided is appropriate for the 404 page.

Here's my 404PageResolver pipeline:

namespace _404Test
{
    public class Page404Resolver : Sitecore.Pipelines.HttpRequest.HttpRequestProcessor
    {
        public override void Process(Sitecore.Pipelines.HttpRequest.HttpRequestArgs args)
        {
            try
            {
                // If current item not available in Sitecore, then
                if (Sitecore.Context.Item == null)
                {
                    // Find an error-page item and set it to context Item
                    Item item404 = Sitecore.Context.Database.GetItem(@"{18492A25-87CA-4064-855E-EADC1460A3B9}");
                    Sitecore.Context.Item = item404;
                    ItemNotFoundStatusRepository.Set(true);
                }
            }
            catch (Exception E)
            {
                Sitecore.Diagnostics.Log.Error(E.Message, "404 Resolver");
            }
        }
    }
}

And here is my 404 Page Resolved pipeline:

namespace _404Test
{
    public class Page404Resolved : Sitecore.Pipelines.HttpRequest.HttpRequestProcessor
    {
        public override void Process(HttpRequestArgs args)
        {
            if (ItemNotFoundStatusRepository.Get())
            {
                HttpContext.Current.Response.StatusCode = (int)HttpStatusCode.NotFound;
                HttpContext.Current.Response.TrySkipIisCustomErrors = true;
            }
        }
    }
}

Here's the class that I borrowed from Anders Laub

namespace _404Test
{
    public class ItemNotFoundStatusRepository
    {
        public const string NotFoundItemPropertyKey = "notFoundItem";

        public static bool Get()
        {
            return HttpContext.Current.Items[NotFoundItemPropertyKey] != null && (bool)HttpContext.Current.Items[NotFoundItemPropertyKey];
        }
        public static void Set(bool status)
        {
            HttpContext.Current.Items[NotFoundItemPropertyKey] = status;
        }
    }
}

Finally, here's the configuration override that you'll need for your pipelines:

<configuration xmlns:x="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
        <httpRequestBegin>
            <processor type="_404Test.page404resolver, 404Test" x:before="processor[@type='Sitecore.ExperienceAnalytics.Pipelines.HttpRequest.RedirectRequest, Sitecore.ExperienceAnalytics']" />
        </httpRequestBegin>
        <httpRequestProcessed>
        <processor type="_404Test.page404resolved, 404Test" />
      </httpRequestProcessed>
    </pipelines>    
  </sitecore>
</configuration>

When I put this altogether, I was able to get (and understand) how to get the 404 error code that I was looking for without a lot of extra overhead.

4

Here is the Sitecore module, if it helps:

https://marketplace.sitecore.net/Modules/H/Handling_Custom_404_page_in_Multilingual_Environment.aspx?sc_lang=en

I have written a blog post on this, might be useful for someone in future.

http://www.navavayas.com/blog/better-way-of-handling-sitecore-404-pages/

The approach i have taken is :

  • Add a processor after HttpRequest.ItemResolver on HttpRequestBegin pipeline.
  • Find if context item is null, if null set 404 content page as context item.
  • Add another processor in the pipeline httpRequestEnd after EndDiagnostics processor to set the 404 status code for not found Item.

With this approach, we don't need to set 404 status code on Renderings. As we are setting it at the end of "httpRequestEnd" pipeline. /404 page returns 200 status code as this page suppose to return 200 status code.

works in multi-site/multi-language environment.

1
  • 7
    Funny, we used that approach for a while, but our SEO person told us that 404 pages should return 404 Status Code otherwise it will get indexed by search engines (bing.com/webmaster/help/404-pages-best-practices-1c9f53b3) so we reverted back to our original method of adding it from the component. Adding a check in the httpRequestEnd pipeline adds a small overhead for every single request so we did not go that route.
    – jammykam
    Commented Dec 21, 2016 at 19:28
1

The other answers provide good solutions for 404 error processor. You can create a processor to capture mvc exceptions and show static error page as well. Pipeline -

<mvc.exception>
            <processor type="Sitecore.Mvc.Pipelines.MvcEvents.Exception.ShowAspNetErrorMessage, Sitecore.Mvc">
                <patch:attribute name="type">FWD.Foundation.SitecoreExtensions.Pipelines.Handle500ErrorProcessor, FWD.Foundation.SitecoreExtensions</patch:attribute>
            </processor>
        </mvc.exception>

Code -

public class Handle500ErrorProcessor : ExceptionProcessor
{
    /// <summary>
    /// Custom 500 error processor
    /// </summary>
    /// <param name="args"></param>
    public override void Process(ExceptionArgs args)
    {
        var customErrorsSection = (CustomErrorsSection)ConfigurationManager.GetSection("system.web/customErrors");
        var context = args.ExceptionContext;
        var httpContext = context.HttpContext;
        var exception = context.Exception;

        if (customErrorsSection.Mode != CustomErrorsMode.Off)
        {
            if (context.ExceptionHandled || httpContext == null || exception == null)
            {
                return;
            }

            var exceptionInfo = GetExceptionInfo(httpContext, exception);

            // Log the error
            Logger.Instance.LogError(string.Format("(Executing 500 error page) There was an error in {0} : {1}", Sitecore.Context.Site.Name, exceptionInfo), exception);

            // Return a 500 status code and execute the custom error page.
            httpContext.Server.ClearError();
            //set your status code here, if needed            
            // Get the physical error file else show default sitecore error page                
            httpContext.Server.Execute(Sitecore.Configuration.Settings.GetSetting("ErrorPage", @"\sitecore\service\error.aspx"));
        }
    }

    /// <summary>
    /// Capturing error details
    /// </summary>
    /// <param name="httpContext"></param>
    /// <param name="exception"></param>
    /// <returns></returns>
    private static string GetExceptionInfo(HttpContextBase httpContext, Exception exception)
    {            
        var errorInfo = new StringBuilder();
        errorInfo.AppendLine(string.Concat("URL: ", httpContext.Request.Url));            
        errorInfo.AppendLine(string.Concat("Source: ", exception.Source));
        errorInfo.AppendLine(string.Concat("Message: ", exception.Message));
        errorInfo.AppendLine(string.Concat("Stacktrace: ", exception.StackTrace));
        return errorInfo.ToString();
    }
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.