Abschnitt 7 · Lektion 5
Fehler im Betrieb
Bisher ging es darum, Fehler zu erkennen und zu behandeln. Jetzt geht es um die Frage, wer sie zu sehen bekommt. Und das ist keine Geschmacksfrage, sondern die erste Sicherheitsentscheidung dieses Kurses.
Zwei Schalter
PHP hat für Meldungen zwei getrennte Einstellungen, und die verwechselt man leicht:
display_errors bestimmt, ob eine Meldung in die Ausgabe geschrieben wird, also mitten in das,
was dein Programm sonst produziert. log_errors bestimmt, ob sie protokolliert wird, damit du
später nachsehen kannst.
Auf deinem eigenen Rechner willst du beides an. Auf einem Server im Netz willst du display_errors
aus und log_errors an. Auf der Kommandozeile dieses Kurses steht es genau so, und deshalb
hast du Meldungen bisher immer im Terminal auf dem Fehlerkanal gefunden und nie in der Ausgabe.
Der Server, der in dieser Lektion läuft, ist dagegen ein Entwicklungsserver und hat die Anzeige
an: In seiner Startzeile im Terminal steht -d display_errors=1, und das heißt, dass
Meldungen auf der Seite landen. Genau das führt das erste Beispiel vor, und das zweite schaltet es
wieder ab.
Wie das aussieht, wenn jemand es vergisst
<?php
// So steht ein Server eingestellt, auf dem jemand vergessen hat,
// die Anzeige auszuschalten. Der Server dieses Kurses hat sie
// ohnehin an; die Zeile hier wirkt auch bei php index.php.
ini_set("display_errors", "1");
$warenkorb = ["Kabel" => 2, "Maus" => 1];
echo "<style>body{font-family:sans-serif;max-width:34em;margin:2rem auto}</style>\n";
echo "<h1>Dein Warenkorb</h1>\n";
echo "<p>Kabel: ", $warenkorb["Kabel"], " Stueck</p>\n";
echo "<p>Monitor: ", $warenkorb["Monitor"], " Stueck</p>\n";
echo "<p>Vielen Dank fuer deine Bestellung.</p>\n"; Hier läuft ein Server, als einzigem Beispiel in diesem Abschnitt. Dafür gibt es einen Grund: Der Satz „eine Fehlermeldung hat auf einer öffentlichen Seite nichts zu suchen” ist eine Behauptung, solange man die Seite nicht sieht. Also sieh sie dir an.
Das Beispiel schaltet display_errors ausdrücklich ein, so wie es auf vielen Servern eingestellt
ist, bei denen niemand nachgesehen hat. Der Fehler selbst ist harmlos, ein Schlüssel, den es nicht
gibt. Nur steht die Meldung jetzt im Absatz: zwischen Monitor: und Stueck klebt ein Satz
mit dem vollen Pfad der Datei und einer Zeilennummer, fett und mit Warning davor.
Sieh dir beide Stellen an. Im Reiter Browser steht die Seite, so wie ein Besucher sie sähe. Tipp
dann php index.php im Terminal: Dort steht dieselbe Ausgabe als Zeichen, und du erkennst, dass
die Meldung wirklich Teil des Dokuments ist. Auf dem Fehlerkanal steht sie außerdem, also zweimal.
In den anderen vier Lektionen dieses Abschnitts läuft kein Server, und das ist kein Ausfall. Dort ist die Ausgabe im Terminal die Sache, hier ist es die Seite.
<?php
// So gehoert es auf einen Server im Netz: Anzeige aus, das
// Protokoll bleibt an. Dieselbe Seite, derselbe Fehler.
ini_set("display_errors", "0");
$warenkorb = ["Kabel" => 2, "Maus" => 1];
echo "<style>body{font-family:sans-serif;max-width:34em;margin:2rem auto}</style>\n";
echo "<h1>Dein Warenkorb</h1>\n";
echo "<p>Kabel: ", $warenkorb["Kabel"], " Stueck</p>\n";
echo "<p>Monitor: ", $warenkorb["Monitor"], " Stueck</p>\n";
echo "<p>Vielen Dank fuer deine Bestellung.</p>\n"; Dasselbe Programm, nur steht der Schalter jetzt auf 0. Der Fehler ist immer noch da, aber die
Seite ist sauber: Es fehlt nur eine Zahl im Absatz, und das ist ein Problem, das man in Ruhe beheben
kann. Die Meldung ist damit nicht weg, sie geht ins Protokoll. Tipp auch hier php index.php im
Terminal: Die Ausgabe ist sauber, und auf dem Fehlerkanal steht die Warnung trotzdem.
Was eine Meldung verrät
Warum ist das schlimm? Eine Fehlermeldung ist eine erstaunlich ergiebige Auskunft über dein System. Sie nennt Verzeichnisse und Dateinamen, sie verrät, welche Funktion welche andere aufruft, und bei einem Datenbankfehler nennt sie Tabellen- und Spaltennamen.
Und der Stacktrace geht noch weiter: Er schreibt zu jedem Aufruf die Argumente dazu, mit denen
er gemacht wurde. Aus einem verbinde("db.intern", "shop", "s3hr-geheim") wird damit eine Zeile,
in der das Passwort im Klartext steht. Nicht theoretisch, genau so sieht die Ausgabe aus.
Für dich ist das die Information, die du zum Reparieren brauchst. Für jemanden, der eine Lücke sucht, ist es der Bauplan. Wie sich damit arbeiten lässt, steht in Abschnitt 15.
Deshalb die Regel, die ab hier gilt: Der Besucher bekommt einen ruhigen Satz, du bekommst die Einzelheiten.
Der ruhige Satz und das Protokoll
<?php
function zahleAus(int $cent): void
{
if ($cent <= 0) {
error_log("zahleAus: unzulaessiger Betrag " . $cent);
echo "Die Auszahlung hat nicht geklappt.\n";
return;
}
echo "Ausgezahlt: ", number_format($cent / 100, 2, ",", "."), " Euro\n";
}
zahleAus(2500);
zahleAus(-40);
echo "Fertig.\n"; error_log() ist die einfachste Form davon. Auf einem Server landet der Text in der Protokolldatei,
in diesem Kurs auf dem Fehlerkanal, den du im Terminal siehst. Er geht nie in die Ausgabe. Wenn du
die beiden Kanäle einmal getrennt sehen willst: php index.php 2>/dev/null zeigt nur, was der
Benutzer bekommt, php index.php 2>&1 >/dev/null nur das Protokoll.
Damit hast du die Aufteilung, um die es geht: Der Benutzer liest Die Auszahlung hat nicht geklappt., und du liest zahleAus: unzulaessiger Betrag -40. Zwei Sätze, zwei Zielgruppen, ein
Vorfall.
Schreib in den Protokolleintrag mehr, als die Ausnahme selbst hergibt. Division by zero allein
hilft dir nicht weiter; interessant ist, wo und womit. Der Name der Funktion und der Wert,
der das ausgelöst hat, sind das Minimum.
Und für alles, woran du nicht gedacht hast
<?php
set_exception_handler(function (Throwable $fehler): void {
error_log("Unbehandelt: " . get_class($fehler) . ": " . $fehler->getMessage());
echo "Es ist ein Fehler aufgetreten. Bitte spaeter erneut versuchen.\n";
});
echo "Der Auftrag wird bearbeitet.\n";
throw new RuntimeException("Verbindung zur Datenbank verloren");
echo "Diese Zeile wird nie ausgegeben.\n"; set_exception_handler() legt eine Funktion fest, die PHP aufruft, wenn eine Ausnahme nirgends
gefangen wurde. Statt Abbruch mit Exit-Code 255 und einem Stacktrace bekommst du deine eigene Meldung
und Exit-Code 0 (php index.php; echo $? im Terminal bestätigt die Null).
Zwei Dinge dazu, beide wichtig. Erstens: Das Programm läuft danach trotzdem nicht weiter, die Zeile
hinter dem throw wird nie ausgeführt. Der Handler ist kein catch, sondern das Letzte, was
passiert. Zweitens: Er ersetzt keine richtige Fehlerbehandlung. Er ist die Auffangdecke für den Fall,
dass du einen Fall übersehen hast, und den übersiehst du garantiert.
In Abschnitt 16 baust du genau so etwas für deine eigene kleine Anwendung, und dann wirst du diese Zeilen wiedererkennen.
Zum Mitnehmen
Auf deinem eigenen Rechner willst du jede Meldung sofort sehen. Auf einem Server im Netz will sie niemand sehen außer dir, und zwar später, im Protokoll. Zwei Schalter entscheiden darüber, und diese Lektion ist die einzige des Abschnitts, in der du das Ergebnis im Browser als Seite vor dir hast.
Jetzt du
Basis Konto, kostenlosZu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.
Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.
In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.
Was in dieser Lektion steckt
-
Artikel mit 4 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe, dein Code läuft auf einem Server
Öffnet sich mit dem Basis Konto.