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.
? how do you patch theMediaRequestHandler
?handlers
part.. where are them? what consequences may i have if i comment the lines that the article says to comment?handlers
section?