-
-
Save timw255/f0c10de9be1dcb98b0d6 to your computer and use it in GitHub Desktop.
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> |
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.
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.
_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.