Artikel

Die Lizenzverwaltung war seit Wochen funktionslos

Genehmigen, Ablehnen, Sperren, Entsperren — nichts davon tat etwas. Die Ursache war eine Zeile in der Eingabeprüfung, und der Grund, warum es niemand merkte, ist die eigentliche Lehre.

BlackZackBlackzack

1028 Wörter · 5 Min. Lesezeit

  • yurna
  • api
  • fehlersuche

Ich wollte einen Lizenzantrag genehmigen. Klick auf den Knopf, kurze Pause, nichts. Die Liste sah aus wie vorher. Zweiter Versuch, dasselbe.

Was dann herauskam, war unangenehmer als der Fehler selbst: Diese Funktionen hatten seit Wochen nichts getan. Nicht seit heute, nicht seit dem letzten Update — seit ich die Verwaltungsoberfläche benutze.

Der Fehler

Meine Verwaltungsschnittstelle prüft eingehende Daten gegen ein Schema. Der Ablauf war:

const daten = await c.req.json();      // Anfragekörper lesen
const geprueft = schema.parse(daten);  // gegen das Schema prüfen

Das Lesen des Körpers wirft einen Fehler, wenn der Körper leer ist. Und genau das war der Fall: Mehrere Lizenzaktionen haben gar keine Pflichtfelder. Sperren braucht nur die Kennung, die im Adresspfad steht. Genehmigen ebenso.

Ein Aufruf ohne Körper ist für solche Aktionen völlig legitimes HTTP — und genau so hat mein eigenes Panel sie aufgerufen. Ergebnis: eine Fehlerantwort über ungültiges JSON, bei jeder einzelnen dieser Aktionen.

Betroffen waren sechs Lizenzrouten plus zwei Routen zum Sperren und Freigeben von Nutzern. Nicht betroffen war alles, was ausdrücklich Daten mitschickt — deshalb funktionierte der Rest der Verwaltung tadellos.

Warum es wochenlang niemand gemerkt hat

Hier liegt die eigentliche Geschichte.

Das Panel hat Fehlschläge verschluckt. Man klickte auf „Genehmigen", und es passierte sichtbar nichts. Keine Fehlermeldung, kein roter Hinweis, kein Ladezustand, der in einem Ergebnis endet. Die Anfrage lief, kam mit einem Fehler zurück, und die Oberfläche hat den Fehler nicht angezeigt.

Ich habe die Wirkung nicht geprüft. Nach dem Klick habe ich die Seite nicht neu geladen, um zu sehen, ob der Antrag jetzt genehmigt ist. Warum auch — ich hatte ja geklickt.

Es gab wenige Anlässe. Lizenzanträge sind selten. Zwischen zwei Gelegenheiten liegen Tage.

Diese drei Punkte zusammen ergeben einen Fehler, der beliebig lange überleben kann. Und die Reparatur des dritten Punkts ist unmöglich, die des ersten trivial.

Warnung

Eine Oberfläche, die Fehlschläge verschluckt, verlängert jeden Fehler um Wochen. Jeder Knopf braucht drei Zustände: bereit, läuft, Ergebnis. Und das Ergebnis muss beides können — Erfolg und Fehler, mit Text. Das ist keine Kosmetik, sondern die Voraussetzung dafür, dass Fehler überhaupt sichtbar werden. Ich hatte das für die Fassung des Panels auf der Liste stehen und für weniger wichtig gehalten als neue Ansichten. Das war falsch.

Die Behebung

Zwei Zeilen an der richtigen Stelle:

async function koerperLesen(c) {
  try {
    const text = await c.req.text();
    return text.trim() === "" ? {} : JSON.parse(text);
  } catch {
    return null;  // wirklich kaputtes JSON — das ist ein Fehler
  }
}

Ein fehlender oder leerer Körper wird als leeres Objekt behandelt und dem Schema vorgelegt. Schemata mit Pflichtfeldern scheitern weiterhin — dann aber mit einer aussagekräftigen Meldung samt Feldliste statt mit einer Beschwerde über JSON.

Der Unterschied ist wichtig: Vorher scheiterte die Anfrage, bevor die fachliche Prüfung stattfand. Jetzt scheitert sie in der Prüfung, mit einer Begründung, die zur Sache gehört.

Was ich zusätzlich geändert habe

Jeder Knopf im Panel zeigt jetzt sein Ergebnis. Erfolg als kurze Bestätigung, Fehler mit Text und Fehlercode. Der Zustand bleibt stehen, bis man ihn wegklickt.

Die Liste lädt nach einer Aktion neu. Damit sieht man die Wirkung, nicht nur die Meldung.

Eine Prüfung im Panel gegen die Schnittstelle. Ein kleiner Ablauf, der beim Start jede Aktion einmal gegen einen Testdatensatz ausführt. Damit fällt so etwas beim nächsten Mal innerhalb von Sekunden auf.

Was ich an der Eingabeprüfung grundsätzlich geändert habe

Der Fehler hat mich dazu gebracht, meine Validierung insgesamt zu überdenken. Vorher war sie eine Kette, in der jeder Schritt scheitern konnte, ohne dass klar war, welcher.

Jetzt läuft sie in einer festen Reihenfolge, und jeder Schritt hat einen eigenen Fehlercode:

1. Rumpf lesen. Fehlend oder leer ist zulässig und wird zu einem leeren Objekt. Nur echtes, kaputtes JSON ist hier ein Fehler.

2. Schema prüfen. Pflichtfelder, Typen, Wertebereiche. Fehler nennen die betroffenen Felder.

3. Fachliche Prüfung. Existiert der Datensatz? Ist der Übergang erlaubt?

4. Berechtigung. Darf der Aufrufende das mit diesem Datensatz?

Die Reihenfolge ist wichtig: Eine Fehlermeldung über ein fehlendes Feld ist nützlicher als eine über einen nicht existierenden Datensatz, wenn beides zutrifft. Und die Berechtigung kommt zuletzt, weil man einem Unberechtigten nicht durch unterschiedliche Fehlermeldungen verraten sollte, welche Datensätze es gibt.

Der Test, der so etwas gefunden hätte

Es gibt eine Testart, die ich vorher nicht hatte und die genau diesen Fehler in Sekunden gezeigt hätte: jeder Endpunkt einmal so aufgerufen, wie ihn die eigene Oberfläche aufruft.

Nicht mit sorgfältig zusammengestellten Testdaten, sondern mit dem, was der Client tatsächlich schickt. Bei mir hätte das bedeutet: Aufruf ohne Rumpf, weil das Panel es so macht.

Ich habe daraus einen kleinen Durchlauf gebaut, der die wichtigsten Aktionen gegen einen Testdatensatz ausführt und prüft, dass eine Antwort im Erfolgsbereich zurückkommt. Er läuft in wenigen Sekunden.

Der Wert liegt nicht in der Gründlichkeit — er prüft nur den Normalfall. Er liegt darin, dass er den Normalfall so prüft, wie er tatsächlich vorkommt. Genau dort war meine Lücke: Ich hatte den Endpunkt getestet, aber nicht so, wie ihn der einzige echte Client benutzt.

Woran ich stille Fehlschläge heute erkenne

Nach diesem Fall achte ich auf drei Anzeichen, die alle nichts mit Fehlermeldungen zu tun haben.

Eine Funktion, die niemand erwähnt. Wenn ein Bereich seit Wochen nicht in Gesprächen vorkommt, kann das bedeuten, dass er reibungslos läuft — oder dass ihn niemand benutzt, weil er nicht funktioniert.

Ein Klick, nach dem man die Seite neu lädt. Wenn ich mir selbst angewöhnt habe, nach einer Aktion neu zu laden, um zu sehen, ob sie gewirkt hat, ist die Rückmeldung unzureichend — und wo die Rückmeldung fehlt, kann ein Fehlschlag unbemerkt bleiben.

Zahlen, die sich nicht ändern. Ein Zähler, der seit Wochen gleich steht, ist entweder ein Feature, das niemand nutzt, oder ein Vorgang, der nichts mehr schreibt.

Der zweite Punkt ist der praktischste. Er kostet nichts und ist ein zuverlässiger Hinweis: Wenn ich meiner eigenen Oberfläche nicht glaube, hat sie ein Problem.

Die Lehre, die über den Fehler hinausgeht

Stille Fehlschläge sind teurer als laute Abstürze. Ein Absturz wird bemerkt und behoben. Ein Klick, der nichts tut, wird als „hat wohl geklappt" abgelegt.

Legitime leere Anfragen sind ein Sonderfall, an den niemand denkt. Wer Eingabeprüfung baut, denkt an falsche Daten, fehlende Felder, falsche Typen. Dass gar keine Daten kommen und das richtig ist, kommt in dieser Aufzählung nicht vor.

Man testet, was man erwartet. Ich hatte die Schnittstelle getestet — mit Aufrufen, die Daten mitschicken. Der Fall ohne Daten kam in meinen Tests nicht vor, weil er in meiner Vorstellung nicht existierte.