V tiché poctě před pár dny zemřelému Miloslavu Švandrlíkovi se musíme nejprve zeptat: "A co si predstavujete pod takým slovom 'správné', Kefalín?" Takže si hned na začátku definujme, že chceme mít možnost přio výskytu HTTP chyby (typicky 404 a 500) poslat uživateli náš vlastní obsah, ovšem se správným stavovým kódem. To v ASP.NET a IIS sice lze zařídit, ale způsobem dost netriviálním.
Nejčastější chyby, s nimiž se uživatel na webu setká, jakou ty se stavovými kódy 404 (Object Not Found, stránka nenalezena) a 500 (Internal Server Error, vnitřní chyba serveru). Samozřejmě je možné se spokojit se standardní systémovou hláškou, ale obecně působí profesionálnějším dojmem, když jsou chybová hlášení nějak ošetřena.
Nastavení vlastních chybových hlášení
V případě ASP.NET vypadá klasická konstrukce ve web.configu takto:
<system.web>
<customErrors mode="RemoteOnly">
<error statusCode="404" redirect="~/_err_404.htm" />
<error statusCode="500" redirect="~/_err_500.htm" />
</customErrors>
</system.web>
Toto řešení má dvě nevýhody. Za prvé, funguje jenom pro .NETové stránky. Tj. pokud se uživatel zeptá na /notfound.aspx
, zobrazí se mu naše vlastní hláška, ale zeptá-li se na /notfound.htm
, ukáže se mu systémová. Za druhé, uživatelův prohlížeč se vlastně nedozví, že stránka neexistuje. Server totiž nevrátí stavový kód 404 pro chybu, ale 302 Found, kterým uživatele přesměruje na adresu /_err_404.htm?aspxerrorpath=/notfound.aspx
, kde načte běžnou stránku se stavovým kódem 200 OK.
První zmíněný problém lze vyřešit celkem snadno. Stačí přidat obsluhu chyby ještě do sekce system.webServer
, na úrovni web serveru, ne na úrovni ASP.NET:
<system.webServer>
<httpErrors errorMode="DetailedLocalOnly">
<remove statusCode="404" subStatusCode="-1" />
<remove statusCode="500" subStatusCode="-1"/>
<error statusCode="404" path="_err_404.htm" responseMode="File" />
<error statusCode="500" path="_err_500.htm" responseMode="File" />
</httpErrors>
</system.webServer>
Toto nastavení se bude vztahovat na ne-ASP.NET stránky. Z mně neznámých důvodů se neumí ani nejnovější verze IIS s ASP.NET dohodnout na jednotné obsluze chyb a je nutné tedy tyto věci nastavovat dvakrát.
Řešení problémů se stavovým kódem
Proč nám vlastně tolik vadí, že uživatel nedostane správný stavový kód, ale je přesměrován? Uživateli samotnému, pokud je to běžný člověk u běžného prohlížeče, to nijak nevadí, že jde o chybu mu (doufejme) dojde z textu stránky. Problém to ale představuje pro roboty: vyhledávače, link-checkery, svým způsobem i statistiky… Tyto věci nám mohou pomoci odhalit problémy na našem webu – chybné odkazy, například.
V případě chyb zpracovávaných web serverem, tedy mimo ASP.NET, si vystačíme s výše uvedenou konfigurací, správný stavový kód server vrátí automaticky. ASP.NET je třeba pomoci.
S verzí 3.5 SP1 se objevil v elementu customErrors
nedokumentovaný atribut redirectMode
. V MSDN library o něm nenajdete ani zmínku, ale funguje a zná ho i IntelliSense. Nastavíme-li jej na hodnotu ResponseRewrite
(výchozí je ResponseRedirect
), nebude uživatel na chybovou stránku přesměrován, ale interně se přepíše:
<customErrors mode="RemoteOnly" redirectMode="ResponseRewrite">
<error statusCode="404" redirect="~/_err_404.htm" />
<error statusCode="500" redirect="~/_err_500.htm" />
</customErrors>
Bohužel, i při tomto nastavení se klientovi pošle stavový kód 200 OK – takže se o chybě systemizovaným způsobem opět nedozví. Nareportoval jsem to jako bug, ale těžko říct, zda to pomůže alespoň v ASP.NET 4.0.
Jak si ale poradit teď? Pomocí HTTP modulu. Vytvoříme HTTP modul, kterým budeme sledovat událost HttpApplication.Error
a v případě HTTP chyby nastavíme správnou odpověď ručně. Jako bonus pak zařídíme i funkcionalitu dodávanou výše zmíněným tajným parametrem pro starší verze frameworku.
Vlastní funkce je poměrně snadná, je to ten jeden řádek, kde nastavujeme Response.StatusCode
. Zbytek HTTP modulu v podstatě jenom čte konfiguraci a pracuje s ní.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Configuration;
namespace Altairis.Web.Management {
public class HttpErrorProcessorModule : IHttpModule {
private const string ERROR_SECTION_NAME = "system.web/customErrors";
private CustomErrorsSection config;
public void Init(HttpApplication context) {
// Check if custom errors are enabled
this.config = WebConfigurationManager.GetSection(ERROR_SECTION_NAME) as CustomErrorsSection;
if (this.config == null || config.Mode == CustomErrorsMode.Off) return;
// Add error handler
context.Error += new EventHandler(context_Error);
}
public void Dispose() {
// NOOP
}
void context_Error(object sender, EventArgs e) {
// Get error information
var app = sender as HttpApplication;
var err = app.Context.Server.GetLastError() as HttpException;
// Check if it's HTTP error
if (err == null) return;
// Check if it's local request
if (app.Request.IsLocal && this.config.Mode == CustomErrorsMode.RemoteOnly) return;
// Transfer to error page
var errorCode = err.GetHttpCode();
var errorRedirectUrl = this.GetErrorRedirectUrl(errorCode);
if (!string.IsNullOrEmpty(errorRedirectUrl)) {
app.Context.Response.StatusCode = errorCode;
app.Context.Server.Transfer(errorRedirectUrl);
}
}
// Helper methods
private string GetErrorRedirectUrl(int errorCode) {
var errorSetting = this.config.Errors[errorCode.ToString()];
if (errorSetting == null) return this.config.DefaultRedirect;
else return errorSetting.Redirect;
}
}
}
Při tvorbě tohoto modulu jsem se nechal inspirovat článkem Helen Emerson, jejíž řešení jsem ovšem značně vylepšil: umí obsluhovat všechny chyby, nejenom 404 a především si korektně poradí s nastavením RemoteOnly
.