Die HTTP-Statuskatzen

Was man mit Microsoft Azure Websites, ASP.NET MVC und KB943791 so alles machen kann

Heute ist wieder einer dieser Tage. Ich so: "Mach ich Internet!". Darauf Microsoft Azure Websites so: "Internal Server Error".

Internal Server Error Status 500

Vielleicht eine verrückte Idee, aber könnte man diese Status-Meldungen nicht etwas freundlicher gestalten?

Bei dem Anblick dieser Fehlermeldung, musste ich sofort an die "HTTP Status Cats“ denken: diese Bilder von Katzen in witzigen Situationen, passend zur Fehlermeldung. Warum also nicht einmal selbst diese Katzenbilder in die Status-Benachrichtigungen einbauen? Aber wie?

Reine Einstellungssache?

Wer jetzt denkt es handelt sich um reine Einstellungssache, liegt zwar gar nicht so falsch:
Ich: Cortana

MSDN - how to override the HTTP Status Messages on Windows Azure Web Sites?

Cortana:

Reine Einstellungssache!

Aber man muss noch weitere Aspekte beachten: Zum einen gibt es Status Messages, die von der Applikation kommen und zum anderen Status Messages, die vom IIS zurückgegeben werden. Außerdem gibt es auch noch einen Error Filter. Da dachte ich mir: "Ok, das werde ich mir nie merken! Also warum nicht einen Blogbeitrag zu diesem Thema schreiben?"

Status Messages

Werfen wir also zunächst einmal einen Blick auf die 404-Statusmeldung, die immer dann auftritt, wenn eine Ressource nicht gefunden werden kann: 404 Not Found.

Generell unterscheidet man bei Webseiten zwischen den zwei Szenarien Staging und Production. „Staging“ bedeutet, dass die Seite in der Entwicklung ist und „Production“ bedeutet, dass die Seite bereits betrieben wird.

Diese Statusmeldung ist nicht nur für den Entwickler interessant, sondern auch für den User der Seite. Im besten Fall erhält der User eine Entschuldigung für den Fehler und die Möglichkeit, den Webauftritt zu durchsuchen.

Für Entwickler ist an der Fehlermeldung 404 eigentlich nur CallStack, Session State und evtl. noch Model State von Interesse. Ist die Seite in der Entwicklung, so wollen Tester und Entwickler aber nicht die 404-Seite des Kunden, sondern die tatsächliche Fehlermeldung bekommen. Viele Entwickler und Administratoren von Webseiten arbeiten daher mit Tools wie ELMAH oder Glimpse.

Das Attribute "mode"

Wie bereits erwähnt, geht es hier um „reine Einstellungssache“, deshalb gehe ich zur Mutter aller Konfigurationsseiten, der Web.config. In der Web.config lassen sich alle benutzerdefinierten Einstellungen für die HTTP-Status Messages vornehmen, die von Applikationsseite aus verwaltet werden können. Über folgende Zeile lässt sich die Konfiguration anpassen:

<customErrors mode="On"> <.../> </customErrors>

Das mode-Attribut gibt Aufschluss darüber, ob benutzerdefinierte Fehler verwendet werden oder nicht und wem welche Fehler angezeigt werden. Das mode-Attribut kann drei Werte aufweisen:

  • On bedeutet, dass benutzerdefinierte Fehler eingeschaltet sind. Sofern kein weiteres defaultRedirect-Attribut gesetzt ist, sieht der User eine generische Fehlermeldung ("Yellow Screen of Death" [Der Bluescreen des Webs]). Diese wird sowohl den Remote Clients, dem Browser des Users als auch dem Local Host angezeigt.

  • Off bedeutet, dass benutzerdefinierte Fehlermeldungen deaktiviert sind und die detaillierten ASP.NET-Fehler sowohl dem Client als auch dem Local Host angezeigt werden.

  • RemoteOnly (Default) bedeutet, dass die benutzerdefinierten Fehlermeldungen nur Remote Clients angezeigt werden. Die ASP.NET-Fehler werden nur lokal angezeigt.

Standardmäßig zeigt eine Website, die im IIS gehostet ist, keine Fehlermeldung an Remote Clients an.

Einfügen der Statuskatzen - nicht so einfach wie gedacht!

Möchten wir nun unsere Statuskatzen einbauen, müssten wir doch eigentlich nur die Fehlermeldungen überschreiben, oder?

<customErrors mode="RemoteOnly"> <error statusCode="404" redirect="http://tailf.blob.core.windows.net/cats/404.jpg"/> </customErrors>

Okay, problem solved! Thanks for the fish! Aber ist es wirklich so einfach? Wenn ein Fehler auftritt, wird die passende Katze angezeigt und die Clients darüber informiert, dass wir hier einen Fehler haben. Okay, für den User ist damit klar, dass hier ein Fehler aufgetreten ist, aber was ist mit Google, Bing bzw. mit Suchmaschinenoptimierung? Bing und Google bekommen ja jetzt nur ein Bild und den Status Code 200. Auch ein Unit Test würde jetzt natürlich schiefgehen. Unglücklicherweise übergibt auch unsere Error Page den Status 200, tatsächlich handelt es sich hier um den BUG:507171.

Der Bug und die Konsequenz daraus

Es gibt zwei Möglichkeiten, diesen Bug zu umgehen. Zum einen ließe sich ein Custom Error Handling implementieren. Dies würde bedeuten, einen entsprechenden Controller zu implementieren (wie hier beschrieben), der je nach Fehlermeldung den Status Code und die passende Katze zurückgibt. Das würde aber nur dann funktionieren, wenn die Applikation lauffähig ist und nicht bereits während des Ladens der Applikation ein Fehler auftritt oder die ganze Applikation abgestürzt ist.

Die zweite Möglichkeit ist die zuverlässigere Methode: Dabei wird auf eine HTML-Seite oder ASPX-Seite weitergeleitet, die den Status Code beinhaltet und das gewünschte Resultat anzeigt.

Young cat can fix this

Young Cat can fix this, cat I'll fix this for you

Gehen wir also nochmal in die Web.config und passen diese entsprechend an:

<customErrors mode="On" redirectMode="ResponseRewrite"> <error statusCode="404" redirect="~/404.aspx"/> </customErrors>

Im Root-Verzeichnis unserer Webapplikation lege ich eine entsprechende ASPX-Seite an. Ich füge ein WebForm hinzu und generiere die entsprechende Datei mit dem Namen 404.aspx.

Das setzen des ResponseRewrite-Attributes in der Web.config erlaubt es mir nun in der 404.aspx-Seite den richtigen Status-Code an Suchmaschinen und Unit Tests auszuliefern.

<% Response.StatusCode = 404; %>

Okay, dieser Ansatz funktioniert, hat aber eine Schwäche, wie im Blogbeitrag von Jan Jonas nachzulesen ist: Sollte aus irgendeinem Grund ASP.NET auf dem Server nicht laufen, war es das leider schon mit unserer Custom Error Page.

Macht aber nichts, es gibt noch einen anderen Weg: Seit IIS 7 gibt es die Möglichkeit, die IIS-Fehlermeldungen via Web.config zu überschreiben. Eine Liste der Statusmeldungen findet man unter Microsoft Support KB 943791.
Im Bereich der web.config füge ich deshalb folgenden Eintrag hinzu:

<httpErrors errorMode="DetailedLocalOnly"> <remove statusCode="404"/> <error statusCode="404" path="/404.html" responseMode="ExecuteURL"/> </httpErrors>

Analog zum "customError Mode"-Attribut gibt es hier ein "errorMode"-Attribut, das entsprechend gesetzt werden kann. Aus der Dokumentation unter IIS customError geht hervor, dass der Wert "Custom" das lokale Testen ermöglicht und "DetailedLocalOnly" der Wert für den Production-Status der Seite ist. DetailedLocalOnly ist auch der vordefinierte Wert. Ich füge meinem Projekt also noch schnell eine HTML-Seite hinzu.

Wenn ich jetzt auf einen Inhalt navigiere, der nicht existiert, erhalte ich eine benutzerdefinierte Seite. Leider ist auch hier der Status-Code erstmal 200. Um das zu umgehen, brauche ich nur das responseMode-Attribut anzupassen und nicht wie bei ASPX den Header überschreiben. Ich setze den responseMode auf File. Die fertige Lösung sieht also wie folgt aus:

<httpErrors errorMode="DetailedLocalOnly"> <remove statusCode="404"/> <error statusCode="404" path="/404.html" responseMode="ExecuteURL"/> </httpErrors>

Welche Konfiguration bei Microsoft Azure Websites überschrieben werden können, kann man in der Applicationhost.config und Rootweb.config nachlesen. Hier habe ich eine Übersichtsliste vom Juli 2014 erstellt:

  • system.webServer.caching
  • system.webServer.defaultDocument
  • system.webServer.directoryBrowse
  • system.webServer.httpErrors
  • system.webServer.httpProtocol
  • system.webServer.httpRedirect
  • system.webServer.security.authorization
  • system.webServer.security.requestFiltering
  • system.webServer.staticContent
  • system.webServer.tracing.traceFailedRequests
  • system.webServer.urlCompression
  • system.webServer.validation
  • system.webServer.rewrite.rules
  • system.webServer.rewrite.outboundRules
  • system.webServer.rewrite.providers
  • system.webServer.rewrite.rewriteMaps
  • system.webServer.externalCache.diskCache
  • system.webServer.handlers Yes, but some are locked
  • system.webServer.modules Yes, but some are locked

Der Spezialfall: Status 500 Message

Meine ursprüngliche Intention war es, die lästige Status 500-Meldung wegzubekommen. Es gibt da einen Spezialfall, den wir uns noch anschauen sollten. Die Projektvorlage von ASP.NET MVC setzt einen globalen Filter (HandleErrorAttribute) auf alle Fehler die während der MVC Pipeline auftreten können. Diese werden dann an ~/views/shared/error.cshtml umgeleitet. Das ist auch in meinem Szenario in Ordnung, da diese Seite typischerweise durch eigene Exceptions aufgerufen wird, wie z.B.:

throw new Exception("Upss");

Es besteht auch die Möglichkeit, im Bereich CustomErrors einen DefaultRedirect-Wert zu setzen. Die Implementierung der Status 500 Message ohne auf den Spezialfall MVC Pipeline zu achten, sieht also wie folgt aus:

<customErrors mode="Off" redirectMode="ResponseRewrite" defaultRedirect="~/500.aspx"> <error statusCode="404" redirect="~/404.aspx"/> <error statusCode="500" redirect="~/500.aspx"/> </customErrors>

Final muss noch die Nicht-ASP.NET-Variante implementiert werden:

<httpErrors errorMode="DetailedLocalOnly"> <remove statusCode="404"/> <error statusCode="404" path="404.html" responseMode="File"/> <remove statusCode="500"/> <error statusCode="500" path="500.html" responseMode="File"/> </httpErrors>

Zu guter Letzt noch eine Zusammenfassung

Am Ende gibt es also viele Wege, die ans Ziel führen:

  1. MVC Pipeline Exceptions, die man über den Controller bzw. das Überladen des Controllers aufrufen kann.
  2. Die Variante über CustomErrors bietet sich insbesondere bei Fehlern an, die keine fatale Auswirkung auf die Applikation selbst haben, der 404-Fehler ist dafür ein gutes Beispiel. Hier kann die Webseite punkten, indem Sie z.B. im Design der Seite eine Suche anbietet.
  3. Der Lösungsweg über httpErrors ist dann zu wählen, wenn man davon ausgeht, dass die komplette Applikation nicht ausgeführt werden kann oder abstürzt, aber der IIS selbst noch Antworten kann. Um ehrlich zu sein, eigentlich ist das mehr ein Feature, als ein Bug. Denken wir mal an ein SharePoint-Szenario, hier gäbe es folgende Technologien, die einen Fehler produzieren könnten: Der IIS als Host der Applikation, ASP.NET als Runtime der SharePoint-Seite, selbstverständlich SharePoint selbst und zu guter Letzt die benutzerdefinierte SharePoint-Applikation. Ohne auch nur den geringsten Plan davon zu haben, wer für den Fehler verantwortlich ist, kann man aufgrund der Fehlermeldung erkennen, in welchem Layer der Fehler Auftritt: IIS, ASP.NET, SharePoint oder Anwendung. Der Support liebt dieses Feature übrigens. ;)

Material

Den Sourcecode und insbesondere die Web.config zum Blogbeitrag findet Ihr unter Github – leider ohne Unit Tests. Wenn ich mal Zeit habe, hole ich das nach (… also wahrscheinlich eher nicht, es sei denn, ich bekomme ganz viele Messages auf Twitter zu diesem Thema ;)).

HTTP-Status-Katzen im Überblick

Die vollständige Liste der HTTP-Status-Codes findet ihr unter iana.org und die IIS-Variante findet ihr in der Knowledge Base unter Microsoft Support KB 943791. Und als kleines Goody zum Schluss, hier nochmal alle Katzen im Überblick.

Quelle: Internet

Http Status Cat 100 Continue Http Status Cat 101 Switching Protocols Http Status Cat 200 OK Http Status Cat 201 Created Http Status Cat 202 Accepted Http Status Cat 204 No Content Http Status Cat 206 Partial Content Http Status Cat 207 Multi-Status Http Status Cat 300 Multiple Choices Http Status Cat 301 Moved Permanently Http Status Cat 302 Found Http Status Cat 303 See Other Http Status Cat 304 Not Modified Http Status Cat 305 Use Proxy Http Status Cat 307 Switch Proxy Http Status Cat 400 Bad Request Http Status Cat 401 Unauthorized Http Status Cat 402 Payment Required Http Status Cat 403 Forbidden Http Status Cat 404 Not Found Http Status Cat 405 Method Not Allowed Http Status Cat 406 Not Acceptable Http Status Cat 408 Request Timeout Http Status Cat 409 Conflict Http Status Cat 410 Gone Http Status Cat 411 Length Required Http Status Cat 413 Request Entity Too Large Http Status Cat 414 Request-URI Too Long Http Status Cat 416 Requested Range Not Satisfiable Http Status Cat 417 Expectation Failed Http Status Cat 418 I'm a teapot Http Status Cat 422 Unprocessable Entity Http Status Cat 423 Locked Http Status Cat 424 Failed Dependency Http Status Cat 425 Unordered Collection Http Status Cat 426 Upgrade Required Http Status Cat 429 Too Many Requests Http Status Cat 431 Request Header Fields Too Large Http Status Cat 444 No Response Http Status Cat 450 Blocked by Windows Parental Controls (Microsoft) Http Status Cat 500 Internal Server Error Http Status Cat 502 Bad Gateway Http Status Cat 503 Service Unavailable Http Status Cat 506 Variant Also Negotiates (RFC 2295) Http Status Cat 507 Insufficient Storage Http Status Cat 508 Loop Detected Http Status Cat 509 Bandwidth Limit Exceeded Http Status Cat 599 Network connect timeout error