Skip to content

Instantly share code, notes, and snippets.

@timw255
Last active August 29, 2015 14:14
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save timw255/f0c10de9be1dcb98b0d6 to your computer and use it in GitHub Desktop.
Save timw255/f0c10de9be1dcb98b0d6 to your computer and use it in GitHub Desktop.
Custom Errors Module - Proof of Concept
protected void Application_Error(object sender, EventArgs e)
{
// check to see if something horrible is going on...
if (Request.Headers["Custom-Error-Request"] != null)
{
// looks like a request to grab an error page ended up here. let's bail.
return;
}
// get the last error
HttpException err = Server.GetLastError() as HttpException;
Server.ClearError();
// try to skip using any other custom error settings
Response.TrySkipIisCustomErrors = true;
// check to see if we need to send back something pretty
var acceptsHtml = Request.Headers["Accept"].Contains("text/html");
// we're on!
if (err != null && acceptsHtml)
{
// get the language from the visitor's browser settings
string lang = (Request.UserLanguages ?? Enumerable.Empty<string>()).FirstOrDefault();
var culture = CultureInfo.GetCultureInfo(lang);
// set the culture to match the browser language
Thread.CurrentThread.CurrentUICulture = culture;
PageManager pageManager = PageManager.GetManager();
PageNode pNode = null;
// this is where we would put a manager to do some fancy lookups based on the error codes
switch (err.GetHttpCode())
{
case 404:
pNode = pageManager.GetPageNodes().Where(pN => pN.Title == "404").FirstOrDefault();
break;
case 500:
default:
pNode = pageManager.GetPageNodes().Where(pN => pN.Title == "Generic").FirstOrDefault();
break;
}
// grab the URL of the error page we're going to display to the visitor
string url = pNode.GetFullUrl(culture, true);
url = UrlPath.ResolveUrl(url, true, true);
// grab the entire HTML of the page
WebClient client = new WebClient { Encoding = Encoding.UTF8 };
client.Headers.Add("Custom-Error-Request", "");
string html = err.GetHttpCode().ToString();
try
{
html = client.DownloadString(url);
}
catch (WebException ex)
{
// the error page is probably blowing up. maybe do some logging...or send an email?
}
// send back the the best response we got for the error and a status code
Response.Write(html);
Response.StatusCode = err.GetHttpCode();
}
else if (err != null)
{
// just fire back a basic message and the status
Response.Write(err.GetHttpCode().ToString());
Response.StatusCode = err.GetHttpCode();
}
}
<customErrors mode="On">
<error statusCode="404" redirect="~/8ec96125-02ce-47e2-a545-0eba4192dc7d" />
</customErrors>
<httpErrors errorMode="Custom">
<remove statusCode="404" subStatusCode="-1" />
<error statusCode="404" prefixLanguageFilePath="" path="/CustomErrorPages/8ec96125-02ce-47e2-a545-0eba4192dc7d.aspx" responseMode="ExecuteURL" />
</httpErrors>
@timw255
Copy link
Author

timw255 commented Feb 2, 2015

_Here's the idea:_
The web.config changes to force 404s to be handled by .NET (as opposed to IIS).
The general idea from the Global.asax.cs can be used to handle and return the correct pages.
A custom module would make it user-configurable in the Sitefinity backend.

@hardye
Copy link

hardye commented Feb 2, 2015

Nice one, Tim. Some comments:

It might be a good idea to check for the Accept header and abort if the value is anything other than text/html or similar. No need to display a custom error page for broken style sheet or image links where no one will ever see the actual error page unless they open their browser console.

Lines 15 to 19: I'm not sure if it is necessary to manually grab the client's language preference. This should have been done by Sitefinity's HTTP module earlier, shouldn't it? If not, you need to account for missing language preferences as well as language prefs that are not covered by the application, i.e. a user has their language pref set to French but the site can only offer English and Spanish -- this requires a fallback. I've built something similar along these lines once (hastily adapted in a text editor and not tested, may contain errors):

private string GetLanguagePreference()
{
    ResourcesConfig config = Config.Get<ResourcesConfig>();
    if (config.Multilingual) // No reason to act if there's only one language anyway
    {
        return null;
    }

    // Abort if there is no accept-language header (search engines etc.)

    string acceptLanguageHeader = Request.ServerVariables["HTTP_ACCEPT_LANGUAGE"];

    if (String.IsNullOrWhiteSpace(acceptLanguageHeader))
    {
        return null;
    }

    // Get the default language of the site            
    string defaultLanguage = config.DefaultCulture.Culture;

    // Abort if the accept-language header contains the default language as the first entry
    if (acceptLanguageHeader.StartsWith(defaultLanguage, StringComparison.InvariantCultureIgnoreCase))
    {
        return null;
    }
    else
    {
        // Do we have any language in our configured languages that matches any of the accept-language header values?

        string targetLang = null;
        string[] acceptLangs = acceptLanguageHeader.Split(new string[] { ",", ";" }, StringSplitOptions.RemoveEmptyEntries);
        int i = 0;

        do // Loop through all the browser-configured languages ...
        {
            string langKey = acceptLangs[i].Substring(0, 2);

            if (!langKey.Equals("q=", StringComparison.InvariantCultureIgnoreCase))
            {
                if (config.Cultures.Values.Any(culture => langKey.Equals(culture.Culture, StringComparison.InvariantCultureIgnoreCase)))
                {
                    targetLang = langKey;
                }
            }

            i += 1;
        } while (String.IsNullOrEmpty(targetLang) && i < acceptLangs.Length);

        return targetLang;
    }
}

Lines 42 to 49: Grabbing the HTML is a lot easier with WebClient, like so:

WebClient client = new WebClient { Encoding = Encoding.UTF8 };
string html = client.DownloadString(pageUrl);

What happens if the custom error page can't be reached and it itself returns 404? Failures need to be accounted for.

@timw255
Copy link
Author

timw255 commented Feb 2, 2015

I hadn't thought about the accept header thing, good call.

The language sniffing was done because I want to grab the page and return it under the same URL, along with the error code. Because I'm not doing a redirect (spinning up the WebRequest manually) I didn't think I could rely on Sitefinity to return the correct page. I admit that I might be over-complicating it though. 😄

If WebClient is faster and returns the same result, I'm totally making the switch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment