IIS and ASP.NET custom fout pagina's, hoe te kiezen
19 feb. 2017
Als .NET web developer heb ik me regelmatig bezig gehouden met fout afhandeling voor websites. Meestal was dat geen makkelijk proces. Er zijn gewoon teveel opties en teveel manieren om de gebruiker te laten weten dat er iets mis ging. Nieuwe (sommige niet zo nieuw meer) technieken zoals MVC.NET, IIS integrated mode en .NET CORE voegde nog meer mogelijkheden toe. Er kan veel gezegd worden over hoe om te gaan met het afvangen en loggen van fouten in code maar ik wil hier alleen focussen op de verschillende mogelijkheden van het tonen van fout informatie naar de gebruiker via custom fout pagina's.
Wat is een goede fout pagina
Een goede fout pagina moet alle voor de gebruiker benodigde informatie tonen over wat er fout ging en indien mogelijk hoe het probleem te verhelpen is. Met benodigde informatie bedoel ik precies genoeg informatie voor de gemiddelde gebruiker om te begrijpen wat er fout ging, dus geen technische details als SQL query xxx on database yyyy failed due to a primary key violation. Geen enkele gebruiker heeft iets aan zulke detail informatie (behalve als de gebruiker de ontwikkelaar van de website is maar als je daarvoor ontwikkeld heb je sowieso een probleem). Een betere melding zou iets kunnen zijn als Uw account kon niet aangemaakt worden. Probeer het later nogmaals. Als het probleem zich blijft voordoen neem dan contact met ons op via xxx-yyy. Afgezien van het feit dat gebruikers geen behoefte hebben aan technische details kan het tonen van teveel details ook een veiligheidsrisico zijn. Weglaten die details dus.
Het is ook raadzaam om waar mogelijk de gebruiker opties aan te bieden om hem verder op weg te helpen. Als er bijvoorbeeld een Internal Server Error (code 500) optreedt die veroorzaakt wordt door een timeout zou je de gebruiker kunnen informeren dat het probleem waarschijnlijk tijdelijk is en dat hij het over een paar minuten nogmaals kan proberen. Een ander voorbeeld is een Page Not Found (code 404) fout, hierbij kan je de gebruiker op weg helpen door alternatieve links aan te bieden naar de informatie die hij waarschijnlijk zocht (dit kan je soms achterhalen door de opgevraagde URL te bekijken). Als je niet kan bepalen wat de gebruiker zocht kan je altijd een link toevoegen naar je zoekpagina (of plaats een zoekveld op je 404 pagina) of naar de eerste bestaande pagina een niveau hoger in de opgevraagde URL (voor een 404 op de link /nieuws/beurs-crash/ zou je naar de /nieuws/ link kunnen verwijzen). Als je je gebruikers echt in alle gevallen zou goed mogelijk support wil bieden dan voeg je op je fout pagina's opties toe om contact met je op te nemen via email, telefoon of chat. Eventueel kan je nog een unieke code toevoegen aan je fout pagina's die gebruikers kunnen doorgeven aan je helpdesk als ze daar contact mee opnemen. Met deze code zou de helpdesk dan in de logs de bijbehorende technische details kunnen opzoeken. Zo ziet de gebruiker geen technische details maar heeft de helpdesk medewerker wel alle details beschikbaar.
Opties om een custom fout pagina te tonen
Nu we hebben vastgesteld wat een fout pagina moet bevatten moeten we onze applicatie vertellen hoe en wanneer welke fout pagina getoond moet worden. In ASP.NET web applicaties (.NET Core laat ik voor nu even buiten beschouwing) heb je grofweg de volgende opties:
- De customErrors sectie in de web.config
- De httpErrors sectie in de web.config
- Routing naar een fout pagina vanuit code
De customErrors sectie in de web.config
De customErrors sectie in de web.config bestaat al heel lang in ASP.NET. Het is onderdeel van de system.web sectie en vangt unhandled exceptions in .NET applicaties af en stuurt requests waarin de exceptions optreden door naar de juiste fout pagina's. Je kan per status code van de error (alle codes vanaf 400) een fout pagina opgeven. Een gemiddelde customErrors sectie ziet er ongeveer als volgt uit:
mode attribuut
Het mode
attribuut kan de volgende waardes hebben:
- Off
- On
- RemoteOnly
Off
schakelt het tonen van custom fout pagina's volledig uit, in het geval van fouten worden de standaard gedetailleerde ASP.NET fout pagina's getoond.
Bij On
worden altijd custom fout pagina's getoond. RemoteOnly
toont alleen custom fout pagina's voor requests die niet afkomstig zijn van de server waarop de website draait, voor requests vanaf de server zelf worden de gedetailleerde ASP.NET fout pagina's getoond. Dit is de standaard en aangeraden instelling hoewel je voor het testen van de fout pagina's in je ontwikkel omgeving de On
moet gebruiken.
redirectMode attribuut
Dit attribuut kan maar 2 waardes hebben:
- Redirect
- ResponseRewrite
De waarde Redirect
doet precies wat je verwacht en redirects de URL waarbij de fout optreed naar de URL die je opgeeft bij het redirect
attribuut van het error
element. Ik raad sterk af deze waarde te gebruiken. De redirect maakt het niet duidelijker voor de gebruiker omdat de URL verandert en het is ook geen goed idee vanuit SEO perspectief om te redirecten naar een fout pagina. De betere en gebruikelijke optie is om hier ResponseRewrite
te gebruiken, hierbij blijft de URL ongewijzigd en wordt de opgegeven fout pagina gerendered als response van het huidige request. Deze optie heeft wel een nadeel. De response geeft een code 200 terug terwijl een fout pagina altijd de juiste fout status code moet teruggeven. De makkelijkste oplossing hiervoor is om een .ASPX pagina te gebruiken als fout pagina (ja, dus ook binnen een MVC applicatie) en daarin handmatig de status code van de response in te stellen.
defaultRedirect attribuut
Met dit attribuut kan je een standaard pagina aangeven voor alle fouten waarvoor niet expliciet een pagina opgegeven is. Als je bijvoorbeeld voor fouten met status code 500 en 404 een fout pagina's opgeven hebt maar er ontstaat een fout met code 401 dan zou de pagina gebruikt worden die je bij dit defaultRedirect
attribuut opgegeven hebt. Het is aan te raden dit attribuut altijd te gebruiken.
De httpErrors sectie in de web.config
Sinds de introductie van Integrated Mode in IIS 7 is er een nieuwe optie bijgekomen om fout pagina's te tonen. Met de httpErrors
sectie (onder de system.webServer
sectie) kan je fout pagina's configureren op IIS server niveau. Het belangrijkste verschil met customErrors
is het feit dat httpErrors
fouten kan afhandelen voor alle requests, niet alleen voor requests die door ASP.NET afgehandeld worden. Dit betekent dat als er bijvoorbeeld iets mis gaat bij het opvragen van een statische file (zoals een afbeelding) die volledig door IIS wordt afgehandeld zonder tussenkomst van ASP.NET code, dat hiervoor ook een custom fout pagina getoond kan worden. Een gemiddelde configuratie voor httpErrors
ziet er als volgt uit:
Met de httpErrors
sectie kan je net als bij customErrors
per status code een fout pagina toewijzen. Het is aan te raden om eerst alle fout pagina's te verwijderen die op hogere niveaus in config files gedefinieerd zijn, dit wordt in het voorbeeld gedaan met <clear />
. De belangrijkste attributen van het httpErrors
root element zijn:
errorMode attribuut
Dit attribuut is vergelijkbaar met het mode
attribuut van customErrors
. Het kan de volgende 3 waardes hebben, Custom
, Detailed
en DetailedLocalOnly
. Normaal kies je hier voor Custom
of voor DetailedLocalOnly
, de eerste optie toont altijd je eigen fout pagina's, de tweede alleen wanneer het request van een buiten de server afkomstig is.
existingResponse attribuut
Als een response een fout status code heeft (>= 400) dan kan httpErrors
iets met deze response doen. ExistingResponse
bepaald of en wanneer er iets met deze responses wordt gedaan. Er zijn weer 3 opties, Auto
, PassThrough
en Replace
. PassThrough
laat de response onveranderd door, Replace
toont altijd een fout pagina (de standaard IIS versie of een eigen opgegeven). De Auto
optie maakt een keuze tussen PassThrough
en Replace
afhankelijk van de waarde van een variabele die SetStatus
heet. Deze variable is vanuit .NET in te stellen met de TrySkipIisCustomErrors
eigenschap van het HttpResponse
object. Als je true
meegeeft zullen er geen fout pagina's door httpErrors
getoond worden, bij false
wel. Later meer hierover.
defaultResponseMode attribuut
Hiermee geef je aan hoe de request naar de fout pagina doorverwezen wordt. Je hebt weer 3 opties, File
, ExecuteUrl
en Redirect
. The File
optie haalt de inhoud van de fout pagina op en toont deze in de response zonder enige verwerking. ExecuteUrl
roept de opgegeven relatieve URL aan en toont het resultaat. Met deze optie kan je .ASPX files als fout pagina gebruiken. Redirect
doet precies wat het zegt en redirect de response naar de opgegeven URL. Het gebruik van deze optie is niet aan te raden onder andere uit SEO perspectief.
defaultPath attribuut
Dit attribuut laat je het pad opgeven waarnaar alle responses moeten worden verwezen die een fout status code hebben maar waarvoor niet expliciet een pad of file is opgegeven. Dit werkt identiek aan het defaultRedirect
attribuut van customErrors
.
error element
Het error element heeft slechts 2 verplichte attributen, statusCode
en path
, deze spreken redelijk voor zich. Let er wel op dat path
afhankelijke van de instelling van defaultResponseMode
een relatief bestands pad of een (relatieve) URL moet zijn. Eventueel kan je ook de response mode per error element instellen met het responseMode
attribuut in plaats van alleen via defaultResponseMode
in de httpErrors
sectie.
Hoe httpErrors fouten afvangt
Een van de belangrijkste verschillen tussen httpErrors
en customErrors
is dat customErrors
daadwerkelijk (.NET) exceptions afvangt terwijl httpErrors
reageert op status codes in de response. Dit verschil heeft mij in het verleden flink wat hoofdbrekens bezorgt toen ik aan een site werkte die recent was over gegaan van IIS classic naar integrated mode. Deze overgang activeerde de httpErrors
functionaliteit en hier hadden we niet goed rekening mee gehouden. Er traden hierdoor problemen op met sommige handlers op de site. Veel handlers waren ontworpen om bij fouten handmatig een 500 status code toe te voegen aan de response en JSON terug te geven met gegevens over de opgetreden fout. HttpErrors
ving deze responses af en gaf HTML fout pagina's terug in plaats van de oorspronkelijke JSON. Het koste me enige tijd om hier achter te komen en vervolgens een oplossing te vinden.
De standaard instelling voor het httpErrors
attribuut existingResponse
is Auto
wat betekend dat alle reponses met een fout status code in principe vervangen worden door een fout pagina van httpErrors
. Behalve als de TrySkipIisCustomErrors
eigenschap van het Response
object in ASP.NET op true
wordt gezet, in dat geval blijft de oorspronkelijke response zoals hij was. In ons geval moesten we in alle handlers die een 500 status code terug konden geven deze eigenschap op true
zetten. Een andere oplossing was geweest om het existingResponse
attribuut van httpErrors
op PassThrough
te zetten zodat geen enkele response met een status code van 500 vervangen werd door een httpErrors
fout pagina. Dit was voor onze site echter niet wenselijk.
Omzeilen van customErrors
HttpErrors
zou een goede vervanging van customErrors
kunnen zijn, het vangt alle errors af, niet alleen degene in .NET code. Als je echter het mode
attribuut van customErrors
op Off
zet met het doel customErrors
uit te zetten dan zal in het geval van een fout een gedetailleerde ASP.NET fout pagina getoond worden en niet een httpErrors
fout pagina. CustomErrors
lijkt TrySkipIisCustomErrors
altijd op true
te zetten waardoor dus httpErrors
fout pagina's nooit getoond worden, althans zolang daar existingResponse
op de standaard stand Auto
staat. De snelste oplossing is om existingResponse
op Replace
te zetten. Vaak is dit een goede oplossing maar dit kan dus problemen geven voor responses die een status code 500 terug geven in combinatie met een custom response body zoals handlers die een JSON result terug geven. In dit geval moet je existingResponse
op Auto
laten staan en je Global.asax zo aanpassen dat in het geval van een exception deze (na eventuele logging) ge-cleared wordt en de response status code op 500 wordt gezet. Omdat er nu wel een status code 500 in de response zit maar geen exception meer, zal customErrors
niks doe met de response maar httpErrors
wel.
3. Routing naar een fout pagina vanuit code
Naast de 2 opties om responses bij fouten naar een fout pagina door te sturen met behulp van web.config configuratie zijn er meerdere mogelijkheden om hetzelfde te bereiken vanuit code. Het gaat hier te ver om deze opties in detail uit te leggen daarom een kort overzicht.
MVC Error attributen
Als je gebruik maakt van MVC.NET kan je het HandleError
attribuut toevoegen aan alle Actions waarvoor je custom fout pagina's wil tonen.
Bij fouten wordt standaard de View Error.cshtml getoond (als deze bestaat natuurlijk). Je kan eventueel een andere View opgeven door de View
parameter van HandleError
te gebruiken, bijvoorbeeld HandleError(View = "500.cshtml")
. HandleError
werkt alleen als customErrors
niet op mode="Off"
staat in de web.config.
Overriding OnException in een MVC controller
Nog een optie voor MVC.NET gebruikers. Voor individuele Controllers of voor een BaseController, als je die gebruikt, kan je de OnException
overriden.
Het Application_Error event
Deze optie werkt zowel voor MVC als Webforms. Je kan dit Event toevoegen aan je Global.asax, alle exceptions die niet ergens eerder in code zijn afgevangen komen hier terecht.
Een HttpModule
Een goede en herbruikbare optie is om een HttpModule te bouwen die exceptions afvangt, deze kan je vervolgens via de web.config aan al je web projecten toevoegen. Veel 3rd party error afhandel extensies (zoals Elmah) gebruiken deze techniek.
Conclusie
Van alle technieken die hier besproken zijn lijkt httpErrors
de beste papieren te hebben. Het werkt voor alle requests, ook voor bijvoorbeeld statische bestanden. CustomErrors
heeft eigenlijk geen enkele meerwaarde meer boven httpErrors
. De MVC error handle opties zijn makkelijk in gebruik maar kunnen alleen fouten in Controllers afhandelen, niet in andere code (voordat de Controller aangeroepen wordt) en ook niet voor statische bestanden. De Global.asax en HttpModules zijn goede alternatieven omdat ze alle fouten afvangen in alle code van de hele applicatie. Maar ze kunnen nog steeds geen fouten buiten de .NET code afvangen hoewel je eventueel HttpModules zo zou kunnen configureren dat alle requests (dus ook voor statische bestanden) afgevangen worden. Dit is zeker een optie maar simpele configuratie van httpErrors
blijft toch de makkelijkste optie. Het is een makkelijk onderhoudbare optie om op een enkele plek je fout pagina's in te stellen. Bovendien zit je niet met dubbele pagina's die je moet onderhouden (bijvoorbeeld een 404 pagina voor customErrors
en een tweede 404 pagina voor IIS statische files voor httpErrors
). Je moet wel met een aantal dingen rekening houden als je httpErrors
inzet, het omzeilen van customErrors
, mogelijke issues met non-HTML responses (zoals JSON handlers) bijvoorbeeld. En vergeet ook niet dat fout pagina's in veel gevallen sowieso overbodig zijn. Er zijn nog steeds veel sites die bij foutieve gebruikers invoer een volledige 500 of 400 fout pagina terug geven. In dit soort situaties kan in bijna alle gevallen beter een nette melding op de pagina zelf getoond worden, dat helpt de gebruiker beter op weg. Voor echte fout pagina's geldt vooral dat je de gebruiker precies genoeg informatie moet geven om hem verder te helpen (Probeer het over 5 minuten nog eens, Misschien helpen de volgende links je verder..., Als het probleem zich blijft voordoen, neem dan contact met ons op via ...). Het is meestal een uitdaging om hiervoor de juiste teksten te bedenken maar uiteindelijk zullen gebruikers je meer waarderen als je goede fout pagina's hebt en het aantal calls naar de helpdesk zou er ook door kunnen verminderen. Fouten zijn slecht maar als ze zich toch voordoen zorg dan dat ze zo duidelijk en behulpzaam mogelijke informatie weergeven.