Niet elke exceptie is een fout, niet elke fout is exceptioneel

Ingediend door Dirk Hornstra op 09-jan-2023 22:03

In onze web-applicaties voegen we logging toe. Hierdoor kunnen we monitoren of er echt iets fout gaat. En kunnen we bij vragen terugzoeken wat er aan de hand was. Tot zover helemaal prima. En die fouten komen ook in bepaalde Slack-kanalen bij ons binnen. Dus ook daar hebben we een actieve real-time monitoring. Dat is goed, totdat je daar meldingen binnen krijgt waarvan je zegt/denkt, die zou ik hier eigenlijk helemaal niet willen zien. Vergelijk het met een alarmsysteem, als er iemand binnen probeert te dringen en aan de klink van de deur zit, er met paperclips in het slot gewriemeld wordt, daar wil je meldingen van ontvangen. Als er de hele tijd een takje van de boom tegen het raam aan tikt en je ook daar elke keer een melding van ontvangt, dan valt die ene inbreker niet meer op omdat je in een stroom van berichten die waarschijnlijk gaat missen.

 

ImageProcessor

ImageProcessor wordt gebruikt binnen Umbraco om afbeeldingen te schalen. Je krijgt via een URL de aanvraag voor een afbeelding binnen (/media/1977/naam-afbeelding.jpg?center=0&mode=crop&width=750&height=562&rnd=133032983970000000). Op basis van de input wordt een geschaalde afbeelding gemaakt. Die actie zit echter niet binnen een try-catch. Wat er hier gebeurde is dat center=0 een ongeldige waarde was. Waardoor er intern buiten de grenzen van een array wat gedaan werd en er (dus) een exceptie teruggegeven werd: IndexOutOfRangeException. Dat is de "inner exception", de aanroepende functie geeft een ImageProcessingException terug.
Omdat dit niet afgevangen werd, werd onze logging actief. Deze logde de "fout" en we kregen een melding in Slack. Maar eigenlijk willen we hier "gewoon" een 404 resultaat terug geven: je probeert een afbeelding op te vragen die niet bestaat (niet met deze parameters), dus een 404 status zou terecht zijn.

Een optie hiervoor zou zijn dat je in de web.config in de elmah-tag het volgende plaatst:



<elmah>
    <errorFilter>
        <test>
            <or>
                <regex binding = "Exception.GetType().FullName" pattern="ImageProcessor.Common.Exceptions.ImageProcessingException" />
            </or>
        </test>
    </errorFilter>
</elmah>
 

Hiermee geef je aan dat een fout van dit type genegeerd kan worden. Dus er wordt geen fout gelogd, maar er blijft wel een "exceptie" actief. Wat wordt afgevangen door de customErrors-tag in je web.config-bestand.
Daar staat meestal een standaard defaultRedirect naar een bepaalde pagina (/er-is-een-fout-opgetreden).
Maar dat geeft dus niet het gewenste resultaat. We hadden net gezegd dat we een 404 status terug wilden geven en niet naar een foutpagina (met status 200).

We hebben (dus) een http-module gemaakt. Die draait mee met alle requests. En in de Init ziet er dan zo uit:



void Init(HttpApplication context)
{
  context.Error += Context_Error;
}
 

Die Context_Error is een eigen functie in die module.
Daar binnen vraag je de laatste fout op, var lastError = application.Server.GetLastError();

Je controleert of lastError.GetType().ToString() gelijk is aan ImageProcessor.Common.Exceptions.ImageProcessingException.
Zo nee, dan niets doen, laat de andere code dit maar afhandelen. Maar is het wel dit type fout, dan doe je eerst een Server.ClearError() om je exceptie op te ruimen.
Vervolgens zet je de Response.StatusCode op 404 en doe je een application.CompleteRequest().

Daarmee krijg je dan nog wel de geconfigureerde "pagina is niet gevonden" te zien, maar deze heeft wel de 404 http status. En als dit een item binnen een afbeelding is, zie je dat niet, maar wel in je netwerk-calls de 404 respons.
Hiermee is de ImageProcessor gefixt!

 

Umbraco Forms

Umbraco Forms gebruik je om binnen je website formulieren te tonen. En te verwerken. En omdat er allemaal botjes online zijn, zitten er bepaalde beveiligingen op. Zo krijg je een bepaalde cookie in je browser, worden een aantal verborgen velden gevuld en wordt zo bepaald of jij "een echte persoon" bent.

Dit valt onder de noemer "Request Forgery". En persoonlijk vind ik het een stuk gebruiksvriendelijker dan dat ik elke keer via een captcha stoplichten in een foto aan moet klikken. Maar goed, als er een botje een foutieve aanroep doet naar de website, dus zonder die cookie, met foutieve waardes in de velden, dan wordt er een exceptie opgeworpen. Soms is een melding wel terecht, als er in de site een foutieve configuratie zit waardoor de pagina (en de componenten daarop) gecached worden, dan kan ook een echte bezoeker zo'n fout triggeren. Maar in 9 van de 10 gevallen is het toch echt een botje wat je site aan het spammen is.

Deze fouten zijn van het type "System.Web.Mvc.HttpAntiForgeryException". Maar er zitten ook een aantal meldingen bij dit volgens mij onder een ander type vallen.
Je zou deze dus ook weer in het errorFilter toe kunnen voegen en het lijkt erop dat dit inderdaad voldoende is, want ik kom dan inderdaad op de "er is een fout opgetreden" pagina.



<elmah>
    <errorFilter>
        <test>
            <or>
                <regex binding = "Exception.GetType().FullName" pattern="System.Web.Mvc.HttpAntiForgeryException" />
            </or>
        </test>
    </errorFilter>
</elmah>
 

Hiermee is Umbraco Forms op zich ook gefixt!


Eigen fouten / selectie van fouten

Maar stel dat dit te veel is.

Nu worden alle fouten van dit type weggedrukt (HttpAntiForgeryException) en misschien heb je een lijstje van 6 meldingen die je regelmatig terug ziet komen en wil je die "muten". Maar als er straks andere meldingen komen, dan wil je die eerst wel in je error-monitoring terug zien.

En zo zag ik ook dat als je in één van de verborgen velden een foutieve waarde invulde je een Invalid value for 'encryptedTicket' parameter. melding krijgt en die is van type System.ArgumentException. Die vang je niet af met System.Web.Mvc.HttpAntiForgeryException. En ik raad je ten zeerste af om System.ArgumentException in het errorFilter toe te voegen, want dit is zo'n algemene exceptie die kan op meerdere plekken in je code opduiken en die wil je zeker in je logs terug zien!

Of je hebt bij de ImageProcessor bepaalde meldingen die je eigenlijk wel zou willen zien (als de bron-afbeelding te groot is, dan zou je die kunnen vervangen bijvoorbeeld).
Of je hebt geen algemeen exceptie-type wat je zo makkelijk in kunt stellen als we hiervoor gedaan hebben. Dan heb je nog een optie, welke mij wat meer zweetdruppels gekost heeft.

Tijdens het debuggen-uitzoeken zag ik namelijk dat mijn Context_Error pas aangeroepen werd nadat de logging al uitgevoerd was. Dan ben je dus te laat.

Maar je hebt nog een functie FirstChanceException.
In je constructor van jouw implementatie van een IHttpModule doe je dit:



public class MyImageExceptionHttpModule : IHttpModule
{
  public MyImageExceptionHttpModule()
  {
    AppDomain.CurrentDomain.FirstChanceException += CurrentDomain_FirstChanceException;
  }
}

Die functie ziet er uit als void CurrentDomain_FirstChanceException(object sender, System.Runtime.ExceptionServices.FirstChanceExceptionEventArgs e)

Je kunt vervolgens controleren in je e.Exception of de .Message één van jouw gefilterde foutmeldingen bevat.
Maar ja, als die overeen komt, dan heb je dus een fout die je wilt "muten". Maar hoe doe je dat dan?

In deze functie kun je eigenlijk alleen maar lezen... behalve de Source! Die kun je wel aanpassen en dat is dus de sleutel.



e.Exception.Source = "NEGEER-DEZE-FOUT";
 

In je errorFilter kun je dit instellen:



<elmah>
    <errorFilter>
        <test>
            <or>
                <regex binding = "Exception.Source" pattern="NEGEER-DEZE-FOUT" />
            </or>
        </test>
    </errorFilter>
</elmah>
 


Uiteindelijk is het resultaat beknopte code geworden. Dat ik er mee heb zitten te pielen kwam door een aantal oorzaken. Mijn collega Leks had al eerder naar het probleem met de ImageProcessor gekeken en had in het project zelf een nieuwe class aangemaakt die erfde van ImageProcessor. Vervolgens had hij daar de functie wel binnen een try-catch gezet, en bij de module ingesteld dat die class uitgevoerd werd, dat werkt ook. Maar we wilden een algemene fix voor onze projecten. Dus hoe doen we dat, want mogelijk heb je verschillende versies en kun je dat niet op een goede manier koppelen. Zo kwam ik op Reflection wat ik wel eerder gebruikt heb, of je nu versie 1 of 2 gebruikt, de naam van het type exceptie blijft hetzelfde.

De focus toen om het via HTTP modules af te vangen. Maar dat gaf niet altijd het gewenste resultaat, zo had ik op een bepaald moment dat je weer terug kwam op de contactpagina. Dat is gevaarlijk, want de bezoeker kan dan denken dat het bericht succesvol verstuurd is.

Toen liep ik tegen het punt aan dat het lokaal wel werkte maar online niet. Dit kwam doordat je ook nog een errorFilter hebt waarbij je lokale fouten negeert (zodat andere developers niet gek worden van jouw debug-fouten):



<equal binding="Context.Request.IsLocal" value="True" type="Boolean"/>
 

Daardoor kwam ik dus op het punt dat ik zag: de elmah-tag in de web.config, ik hier kan zaken instellen om te negeren, kan dat ook op het type? Ja dus!

Mocht je ook een .NET applicatie hebben en Elmah logging gebruiken en tegen hetzelfde probleem aanlopen dat je eigenlijk teveel (onnodige) meldingen krijgt, hopelijk heb ik je hiermee op weg geholpen!