Laden...

Exception-ErrorCode-Mapping

Erstellt von Palladin007 vor 3 Jahren Letzter Beitrag vor 3 Jahren 328 Views
Palladin007 Themenstarter:in
2.078 Beiträge seit 2012
vor 3 Jahren
Exception-ErrorCode-Mapping

Moin,

angenommen, Ihr habt einen Web-Dienst, der Fehler-Codes zurück liefern können soll, wenn etwas schief gegangen ist.
Diese Fehler-Codes sollen konkrete Fehlerquellen identifizieren können, sodass man auf dieser Basis z.B. eine Support-Liste anbieten kann.
Oder wir haben eine Web-API und definieren Fehler-Codes, anhand derer ein Client spezifisch reagieren kann, ohne dass Exception-Details raus gegeben werden müssen.

Wie würdet Ihr das umsetzen?
Einfach in jedem Request eine Reihe von catches und so jeden bekannten Fehler abfangen und um einen Fehler-Code ergänzen?
Oder würdet Ihr eigene Exception-Typen definieren und je Situation die Exceptions fangen und als InnerException weiter geben?

Mich stört dabei, dass...
... man relativ weit oben viele mögliche Exceptions kennen und abfangen können muss, nach einer Änderung im Detail muss man also auch bei diesen catches prüfen und hat Probleme, wenn mehrere Exception-Typen in unterschiedlichen Situationen unterschiedliche Fehler-Codes bedeuten können.
... man für alles Mögliche eigene Exception-Typen definieren muss und ein neues Catch in einer Methode das Exception-Handling der aufrufenden Methode kaputt machen kann, weil die einen anderen Exception-Typ erwartet.

Meine Idee war daher die, dass ganz zum Schluss alle Exceptions generisch (Exception-Typ) gefangen und anschließend der StackTrace abgesucht wird.
Je StackFrame bzw. MethodBase kann man dann mittels Attributen ein Exception-ErrorCode-Mapping aufbauen, das nur für diese Methode gilt.

Das heißt:
Wenn ich eine Methode implementiere, bemerke ich direkt, dass es einige mögliche Fehlerquellen gibt, die ich nicht behandeln kann. Soll einer dieser Fehler als Fehler-Code behandelt werden, kann ich als Attribut definieren, welcher Exception-Typ zu welchem Fehler-Code führen soll, ggf. mit ein paar zusätzlichen Bedingungen, z.B. InnerExceptions.
Wenn die Exception gefangen wird, findet mein System diese MethodBase und das Attribute und kann - wenn die Bedingungen erfüllt sind - den Fehler-Code zurück geben.

Und dann könnte man noch Features einbauen, wie z.B. ...

  • die Möglichkeit, Mappings einer aufgerufenen Methode zu überschreiben oder nicht
  • Suchen von Mappings in der Klasse oder Basis-Methode/Klasse
  • Verschiedene Matching-Strategien (selber Typ, zuweisbar, etc.)

Im Code könnte es so aussehen:


[ErrorCode("ZB001", typeof(FileNotFoundException))]
[ErrorCode("ZB002", typeof(AggregateException), typeof(MyInnerException))]
public void DoWork()
{
    DoInnerWork();
}
private void DoInnerWork()
{
    DoSomethingWithExceptions();
}

Im Catch, wo der Fehler-Code gebraucht wird, sucht man den StackTrace ab, findet "DoWork", prüft, welcher der Mappings passt und nimmt ggf. den Fehler-Code.
Die Performance wird sicher keine Preise gewinnen, aber wenn eine Exception soweit ist, dass ein Fehler-Response geschrieben werden muss, stört das auch nicht mehr.
Mir persönlich gefällt, dass sich an der tatsächlichen Implementierung genau nichts ändert und man somit auch keine Seiteneffekte hat. Außerdem muss eine Methode sich einzig und allein um das kümmern, was sie selber aufruft.

Meine Frage ist jetzt: Was haltet Ihr davon?
Gibt es bessere Wege, so eine Anforderung umzusetzen?
Seht Ihr Probleme bei der Idee?

Beste Grüße

16.807 Beiträge seit 2008
vor 3 Jahren

Hi,

Meine Frage ist jetzt: Was haltet Ihr davon?

Ganz direkt: nichts 🙂
Solche Fehler-Code-Middlewaresysteme haben auf mich immer den Eindruck: es ist ihnen nichts besseres eingefallen oder sie wollten einfach keine "de facto Standards" verwenden.

Grundlegend: der Faktor Http API hier ist erstmal für das Grundsystem egal.

Die Idee, dass es eine Liste von Fehlern gibt, mit denen Codestellen identifiziert werden können, ist ja so alt wie die Dinosaurier - und ich kenne kein System und keine Implementierung, die hier wirklich funktioniert.
Gibts ja schon seit den 70ern und wurde nie besser; weder bei Linux, noch bei Windows und Co.

... man für alles Mögliche eigene Exception-Typen definieren muss und ein neues Catch in einer Methode das Exception-Handling der aufrufenden Methode kaputt machen kann, weil die einen anderen Exception-Typ erwartet.

Das ist ein "Grundproblem" in quasi jeder Software Architektur: auf der einen Seite willst Du eigentlich Fehler so originalgetreu wie möglich behandeln; auf der anderen Seite hast Du die konkurrierende Anforderung der Fehleranreicherung.
Diese beide Anforderungen konkurrieren hart miteinander und sind nicht gemeinsam erreichbar.

Ich persönlich habe aber die Meinung, dass eine spezifische Exception der bessere Weg ist; daher wende ich das aktiv in meinen Projekten so an bzw. empfehle es.
Die Exceptions des Frameworks gelten prinzipiell als Infrastruktur-Exceptions; insbesondere bei den Fehler-Code-Exceptions hast Du womöglich Logik-Exceptions - also eine andere Kategorie, die aus Architektursicht weiter oben ist und selbst implementiert werden sollten, sodass eine Eindeutigkeit erreicht werden kann.

Um darauf zurück zu kommen, warum ich nichts von Fehlercode-System halte:
Ich persönlich glaube nicht dran, dass ein solches System jemals funktionieren wird. Du wirst meiner Meinung nach eine maximal 20% Lösung hin bekommen; aber niemals mehr.

Schauen wir uns kurz die Anforderungen an:

  • Du willst spezifische Exceptions, aus denen man auch schlau wird, zB zuordnen können
  • Du willst dem Client nicht zu viele Infos geben, aber eine Eindeutigkeit herstellen
  • Eigentlich willst Du hier keinen Aufwand haben

Auf was ich setze:

  • Wann immer möglich eigene, spezifische Exceptions an eigenen Schnittstellen verwenden -> Das gilt generell als Best Practise zu Exceptions in .NET
  • Ein Full Structured Logging Framework verwenden, zB Serilog -> Du willst draus schlau werden
  • Ein Tracing System wie Application Insights oder New Relic verwenden -> Du willst es analyisieren können ohne vorher irgendwas machen zu müssen zB. nervige Codes pflegen
    Plus: Der Entwickler soll nichts exlizit tun müssen, weil a) keine Lust drauf hat b) es ohnehin irgendwann vergessen wird und c) sich auf das eigentliche Problem kontrieren soll und nicht auf organisatorischen Overhead

Mit diesen drei Bausteinen kann ich eigentlich alle Anforderungen abdecken, die eine Applikation zur Fehlerermittlung benötigt.

Bei HTTP APIs:
Der HTTP Standard besitzt keinen Mechanismus, um solche spezifischen Codes an einen Client zu übertragen. Auch REST kann das in dieser Form nicht.
Dir bleibt also entsprechend nur das Ergebnis entsprechend anzureichern; in der Regel verwende ich hier einen Meta-Paradigma, der zB Hypermedia-Antworten ähnelt.
In gRPC heisst dieses Vorgehen Richer Error Model.


namespace XXXXX.Runtimes.AspNetCore.Models
{
    public class HttpErrorModel
    {
        public int StatusCode { get; } // HTTP Status Kopie aus der Response
        public string Message { get; } // bla bla Fehler aufgetraucht
        public string Type { get; } // Error Type - damit kann Dein Client was anfangen
        public string CorrelationId { get; } // eindeutige Id - damit kann Dein Support was anfangen
    }
}

In ASP.NET Core gilt, dass Du in Actions eigentlich keine Exceptions behandelst, sondern in der Error Middleware.
Dort würdest Du alle Exceptions, die Du spezifisch zurück geben willst, pflegen. Alle anderen Exceptions enden kann in einem allgemeinen Error-Handling (klassisches switch).
Bei der Pflege mappst Du dann von der Exception auf das HTTP Error Model; dabei kannst Du selbst entscheiden, was Du als Type (Code, Classname.. whatever) serialisierst - zentral an einem Ort.
Wann welche Exception/aka Error Type auftauchen kann, würdest Du dann über die OpenAPI dokumentieren; im Endeffekt also eine übliche Exception-Dokumentation, wie man es in .NET gewöhnt ist - wenn man denn dokumentiert 😉


/// <summary>
/// bla bla
/// </summary>
/// <response code="400">Hier Dokumentation / Verweis der Status Types</response>     
[ProducesResponseType(typeof(HttpErrorModel), StatusCodes.Status400BadRequest)]
public ActionResult<OutputItem> Create(InputItem item)
{

Die CorrelationId wird automatisch durch ASP.NET Core bei jedem Request erzeugt und findet sich auch im Error Logging wieder; Application Insights wird diese Id zB. jeder Log-Operation automatisch angehängt.
So bekommst Du im Zweifel den genauen Zusammenhang des HttpErrorModel zum vollständigen, originalen Operation Kontext incl. Stacktrace und Co.

Im Endeffekt ist diese Umsetzung sehr weit verbreitet und wird in dieser oder ähnlicher Form auch von den Großen eingesetzt.

Palladin007 Themenstarter:in
2.078 Beiträge seit 2012
vor 3 Jahren

es ist ihnen nichts besseres eingefallen oder sie wollten einfach keine "de facto Standards" verwenden.

Naja, weniger die Verweigerung der Standards, als das Fehlen einer Idee, wie ich eine solche Anforderung umsetzen kann.
Ich habe also nicht vor, basierend auf den Codes irgendwelche Logik aufzubauen, aber wenn so ein Code gefordert wird, muss er geliefert werden.

Die Exceptions des Frameworks gelten prinzipiell als Infrastruktur-Exceptions; insbesondere bei den Fehler-Code-Exceptions hast Du womöglich Logik-Exceptions - also eine andere Kategorie, die aus Architektursicht weiter oben ist und selbst implementiert werden sollten, sodass eine Eindeutigkeit erreicht werden kann.

Da liegt wohl mein Denkfehler, über Kategorien bei Exceptions habe ich bisher gar nicht nachgedacht.
Aber es stimmt, wenn ich Business-Logik hinter einer Abstraktionsschicht verberge, gehören Exceptions natürlich mit dazu.

Es sind zwar nicht immer "reine" Logik-Exceptions, aber die Fehler-Codes machen nur dann Sinn, wenn ich von dem Fehler weiß, ihn aber nicht vernünftig behandeln kann.
Dann macht es aber automatisch wieder Sinn, genau das zu trennen, dann könnte man z.B. auch den Schweregrad des Fehlers im Log daran ausrichten.
Blödsinnige Input-Daten sind ja streng genommen kein kritischer Fehler, sondern einfach nur blödsinnige Input-Daten und dann kann ich auch direkt ein paar zusätzliche Fehler-Informationen mitgeben, damit der Client sie auswerten kann.

Ob ich andere Dinge so umsetzen kann (z.B. AppInsights) ist fraglich, ich bin ja schon froh, wenn ich überhaupt DependencyInjection nutzen darf ...
Aber das ist mein Bier, an den schrägen Vorgaben kann ich hoffentlich noch einiges ändern.

Ich danke dir auf jeden Fall für deine ausführliche Antwort, mich hat's auf jeden Fall weiter gebraucht.

16.807 Beiträge seit 2008
vor 3 Jahren

Naja, weniger die Verweigerung der Standards, als das Fehlen einer Idee, wie ich eine solche Anforderung umsetzen kann.

War eher eine Wiedergabe, welchen Eindruck im Allgemeinen ich von solchen Anforderungen hab 😉

Ich habe also nicht vor, basierend auf den Codes irgendwelche Logik aufzubauen, aber wenn so ein Code gefordert wird, muss er geliefert werden.

Wissen denn die Anforderungsgeber, dass es evtl. einfach mittlerweile bessere Alternativen gibt?
Das ist in meinen Augen bei solchen "Uralt-Anforderungen" oft das Problem; sie wissen nicht, was es eigentlich mittlerweile für moderne Wege gibt.
Sie fordern einfach "was sie gewohnt sind". Vielleicht würde hier eine Beratung Deinerseits an diese gut tun. Vielleicht kannst Du damit den ganzen Prozess verschlanken (von Außen kann ich das nicht beurteilen, nur sagen, wie ich vorgehen würde).

Klar; das eine ist das, was heutzutage technisch möglich ist und/oder sich in vielen Dingen durchgesetzt hat - das andere ist das, was man will / verwenden darf.
Typisch deutsches Problem in 99% aller deutschen Firmen: haben wir schon immer so gemacht.

Blödsinnige Input-Daten sind ja streng genommen kein kritischer Fehler, sondern einfach nur blödsinnige Input-Daten und dann kann ich auch direkt ein paar zusätzliche Fehler-Informationen mitgeben, damit der Client sie auswerten kann.

Gibt ja zwei Arten von Input-Fehler: Strukturelle und logische Fehler.
Strukturelle wenn man einfach andere Werte erwartet (int->string: kannst also einfach statisch validieren) oder einfach fehlen -> meistens HTTP 400
Logische Fehler; meist mit DB-Lookup, zB. dass neue Relationen ungültig wären -> viele verwenden 400, wobei 409 oft die bessere Wahl wäre

Palladin007 Themenstarter:in
2.078 Beiträge seit 2012
vor 3 Jahren

War eher eine Wiedergabe, welchen Eindruck im Allgemeinen ich von solchen Anforderungen hab 😉

Den Eindruck habe ich auch und ich hasse es, wenn das Fehler-Handling auf Error-Codes basiert. Im "besten" Fall wird auch noch komplett ignoriert, dass trotzdem Exceptions auftreten können oder die werden generell gefangen und durch einen einzigen Fehler-Code ersetzt.
Deshalb habe ich da zugegeben etwas allergisch reagiert 😁

Wissen denn die Anforderungsgeber, dass es evtl. einfach mittlerweile bessere Alternativen gibt?

Der Kunde:
Vermutlich ja, deshalb gibt es ja das Projekt.

Der Chef:
Definitiv, ich habe es schon oft angesprochen, allerdings gibt's da noch ein paar andere Probleme, die sehr viel weiterreichen, als übliche Konzepte und technische Möglichkeiten.
Wenn dich das interessiert, kann ich es dir privat schreiben, hier möchte ich nicht.