Před nějakou dobou jsem zde uvedl řešení ošetření chyb vlastní chybovou stránkou a ošetření neexistující URL stránkou NotFound (404) v ASP.NET a IIS 7. Popis byl jako součást ukázkové ASP.NET aplikace FileAccessWeb v článku ASP.NET FileAccessWeb Sample, část 4: Ošetření chyb.
Protože požadavky, zkušenosti i technologie ASP.NET se vyvíjí, přináším zde nyní řešení nové. Změna spočívá v tom, že přechod na chybovou stránku byl původně řešen redirectem (stránka měla svojí vlastní URL). Nové řešení dodrží původní URL stránky, na které chyba vznikla, tj. zobrazí se pouze jiný obsah stránky.
Tak jdeme na to. V ASP.NET aplikaci, budu předpokládat vytvořené WebForms stránky Error.aspx a NotFound.aspx, například takto (jejich obsah ale bude závislý na konkrétní aplikaci):
Error.aspx:
<%@ Page Title="Chyba" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" CodeBehind="Error.aspx.cs" Inherits="HandleErrorWeb.Error" %>
<asp:Content ID="BodyContent" ContentPlaceHolderID="MainContent" runat="server">
<h1><%: Title %></h1>
<div class="alert alert-error">
Nastala nedefinovaná chyba. Věříme, že chyba je pouze dočasná.<br />
V opačném případě nás prosím kontaktujte na e-mailu <a href="mailto:">webmaster</a>.
</div>
</asp:Content>
NotFound.aspx:
<%@ Page Title="404 - Stránka nenalezena" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" CodeBehind="NotFound.aspx.cs" Inherits="HandleErrorWeb.NotFound" %>
<asp:Content ID="BodyContent" ContentPlaceHolderID="MainContent" runat="server">
<h1><%: Title %></h1>
<div class="alert alert-error">
Litujeme, ale stránka, kterou jste si chtěli prohlédnout, neexistuje, nebo je dočasně nedostupná.<br />
Pokud by problém přetrvával, kontaktujte nás na e-mailu <a href="mailto:">webmaster</a>.
</div>
<p>Přejít na <a runat="server" href="~/">úvodní stránku</a>.</p>
</asp:Content>
NotFound.aspx.cs:
protected void Page_Load(object sender, EventArgs e)
{
this.Response.StatusCode = 404;
this.Response.StatusDescription = "Not Found";
}
Dále v aplikaci budeme potřebovat definované routy, předpokládám routu na nějakou výchozí stránku a na stránku NotFound.aspx:
//Register routes
var routes = RouteTable.Routes;
routes.MapPageRoute("Default", "", "~/Default.aspx");
routes.MapPageRoute("NotFound", "not-found", "~/NotFound.aspx");
(Na stránku Error.aspx routu potřebovat již nebudeme).
Web.config
Nyní provedeme potřebné nastavení v konfiguračním souboru Web.config jak pro ASP.NET tak IIS 7:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.web>
<compilation debug="true" targetFramework="4.5" />
<httpRuntime targetFramework="4.5" />
<customErrors mode="Off">
<error statusCode="404" redirect="~/not-found" />
</customErrors>
</system.web>
<system.webServer>
<handlers>
<add name="AspxBlockHandler" path="*.aspx" verb="*" type="IMP.Web.NotFoundHandler" />
</handlers>
<httpErrors errorMode="Custom">
<remove statusCode="404" subStatusCode="-1" />
<error statusCode="404" path="/not-found" responseMode="ExecuteURL" prefixLanguageFilePath="" />
</httpErrors>
</system.webServer>
</configuration>
V sekci system.web je element customErrors, u něj ovšem neuvádíme žádný odkaz na Error stránku (tedy bez atributu defaultRedirect), to zařídíme jinak později. Další nastavení stránky NotFound (v customErrors a httpErrors) a blokování aspx stránek (NotFoundHandler) zůstává stejné jako minule (*).
Výše uvedené nastavení můžeme nechat ve výchozím configu, jediné co budeme měnit v Release configu je nastavení customErrors mode="On" (pro debug budeme zobrazovat standardní ASP.NET stránku):
<system.web>
<compilation xdt:Transform="RemoveAttributes(debug)" />
<customErrors mode="On" xdt:Transform="SetAttributes(mode)" />
</system.web>
Zobrazení obsahu chybové stránky
Obsah vlastní chybové stránky zobrazíme v obsluze chyb Application_Error v Global.asax.cs pomoci kódu. Konkrétně k tomu využijeme metodu HttpServerUtility.Transfer, které předáme zdroj stránky načtený pomoci GetCompiledPageInstance.
protected void Application_Error(object sender, EventArgs e)
{
var ex = Server.GetLastError();
if (ex == null)
{
return;
}
var httpex = ex as HttpException;
if (httpex != null && httpex.GetHttpCode() == 404) //404 - file not found
{
return;
}
//Get the customErrors section.
var customErrorsSection = (CustomErrorsSection)ConfigurationManager.GetSection("system.web/customErrors");
if (customErrorsSection == null || customErrorsSection.Mode == CustomErrorsMode.Off)
{
//Výchozí ASP.NET zobrazení chyby (při CustomErrorsMode=Off zobrazí a zaloguje ASP.NET error jinak provede zobrazení error page)
return;
}
//Transfer content to Error page
var errorPageHandler = System.Web.UI.PageParser.GetCompiledPageInstance("~/error.aspx", HttpContext.Current.Server.MapPath("~/error.aspx"), HttpContext.Current);
Server.Transfer(errorPageHandler, true);
}
Kód ošetřuje volání pouze pokud je ve Web.config nastaveno customErrors mode="On". Také si všimněte, že vzniklá Exception není v tomto kódu ošetřovaná (výjimka se šíří dál a může být dále zalogována).
Nyní po vyvolání chyby URL stránky zůstává nezměněná
ELMAH
V minulém řešení kód události Application_Error obsahoval vlastní logování například do eventlogu. To nemusí být někdy vhodné, například pro aplikace nasazované na webové hostingy. Dobrou možností pro logování chyb je použití knihovny ELMAH (Error Logging Modules and Handlers). Ta zařídí nejen vlastní logování chyb (do databáze aplikace), ale obsahuje rovnou i prostředky pro jejích zobrazení v rámci naší aplikace (například pro administrátora).
Knihovnu do projektu přidáme pomoci NuGet balíčku ELMAN (PM> Install-Package elmah) nebo ELMAH on MS SQL Server (PM> Install-Package elmah.sqlserver). Balíček pro SQL server obsahuje navíc soubor Elmah.SqlServer.sql v adresáři App_Readme, tento script spustíme nad databází naší aplikace (ten vytvoří tabulku s názvem ELMAH_Error). Pro SQL server dále ještě musíme mít ve web.config nastavenou sekci connectionStrings a ELMAH na tento connection string nastavit:
<elmah>
...
<errorLog type="Elmah.SqlErrorLog, Elmah" connectionStringName="ConnectionString" />
</elmah>
Pro přístup k zalogovaným chybám slouží odkaz ~/elmah.axd, ten je ve výchozím nastavení povolen pouze pro localhost.
Pro povolení i vzdáleného přístupu musíme v sekci elmah nastavit:
<security allowRemoteAccess="true" />
a dále přístup ke stránce zabezpečit například jen pro administrátory (pokud v aplikaci používáme nějaké přihlašování):
<location path="elmah.axd" inheritInChildApplications="false">
<system.web>
<authorization> <allow roles="Admin" /> <deny users="*" /> </authorization>
</system.web>
<system.webServer>
<handlers>
<add name="ELMAH" verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" preCondition="integratedMode" />
</handlers>
</system.webServer>
</location>
Kompletní zdrojové soubory ukázkového testovacího projektu jsou k dispozici ke stažení zde.
(*) Stránky aplikace se vyvolávají přes URL definovaných pomoci rout (Routes). Zároveň chceme, aby když se uživatel zadanou URL “trefí” na fyzický soubor stránky (tj. soubor s příponou .aspx), bude tato URL zablokovaná. Možná znáte, že na to lze použít vestavěný System.Web.HttpNotFoundHandler, s ním ale pak nechodí zobrazení vlastní NotFound 404 stránky, proto používám vlastní handler NotFoundHandler.
V souboru Web.config je v customErrors a httpErrors pro odkaz na NotFound 404 stránku použita routa, protože odkaz musí být bez .aspx (které jsou blokovány). U httpErrors zde cesta musí být uvedena relativní k celé website, nelze zde použít ASP.NET relativní adresu s ~/.