Abschnitt 16 · Lektion 6
Wenn doch etwas durchkommt
In Lektion 7.5 hast du set_exception_handler() kennengelernt, und dort stand ein Satz, der auf
diesen Abschnitt zeigte: Für deine eigene Anwendung baust du so etwas selbst. Der Front Controller
aus Lektion 16.3 ist der Ort dafür, denn er ist die einzige Datei, durch die jede Anfrage geht.
Vorher lohnt es sich anzusehen, was ohne dieses Netz passiert, und zwar genau.
Die 200 ist das Schlimmste daran
<?php
// Eine ganz normale Seite. Die Funktion unten findet den Preis
// nicht und wirft. Gefangen wird nirgends.
function preisVon(string $artikel): float
{
throw new RuntimeException("Kein Preis fuer " . $artikel);
}
echo "<!doctype html><html lang=\"de\"><head><meta charset=\"utf-8\">";
echo "<title>Preisliste</title></head><body>\n";
echo "<h1>Preisliste</h1>\n";
echo "<p>Kaffee: ", preisVon("kaffee"), " Euro</p>\n";
echo "<p>Diese Zeile kommt nie an.</p>\n";
echo "</body></html>"; Ruf das erste Beispiel auf. Die Überschrift steht da, dann fängt der Absatz an, und mitten im Satz
bricht die Seite ab. Dahinter steht die Meldung mit Stacktrace und dem vollen Pfad der Datei auf dem
Server, weil in diesem Kurs display_errors an ist.
Sieh dir jetzt im Reiter Netzwerk die Statuszeile an. Dort steht 200.
Das ist der Teil, der später wehtut. Eine Überwachung, die deine Seite jede Minute abruft, bekommt ein „alles in Ordnung”. Ein Browser legt die Antwort in seinen Zwischenspeicher, als wäre sie eine richtige Seite. Eine Suchmaschine nimmt die halbe Seite mit Stacktrace in ihren Bestand auf. Und wer sie sieht, liest den Namen deiner Klassen, deine Verzeichnisse und die Zeilennummer.
Schalte display_errors aus, wie es Lektion 15.5 für einen öffentlichen Server verlangt, und es
wird nicht besser, sondern unauffälliger: Dann endet die Seite mitten im Satz und es steht gar nichts
mehr da. Der Status bleibt 200.
Ein Handler ist die halbe Antwort
<?php
// Derselbe Fehler, diesmal mit einer Auffangfunktion. Sieh dir an,
// WO ihre Ausgabe landet, und lies die zweite Warnung.
set_exception_handler(function (Throwable $fehler) {
http_response_code(500);
echo "<p>Da ist etwas schiefgegangen.</p>";
});
function preisVon(string $artikel): float
{
throw new RuntimeException("Kein Preis fuer " . $artikel);
}
echo "<!doctype html><html lang=\"de\"><head><meta charset=\"utf-8\">";
echo "<title>Preisliste</title></head><body>\n";
echo "<h1>Preisliste</h1>\n";
echo "<p>Kaffee: ", preisVon("kaffee"), " Euro</p>\n";
echo "</body></html>"; Das zweite Beispiel hängt eine Auffangfunktion ein, genau die aus Lektion 7.5. Sie wird auch gerufen, das siehst du an ihrem Satz. Nur steht der jetzt hinter der halben Seite, angeklebt an den Absatz, der nie fertig wurde.
Und dazwischen steht die eigentliche Auskunft:
http_response_code(): Cannot set response code - headers already sent, und dahinter in Klammern
die Zeile, ab der die Ausgabe lief.
Das ist derselbe Fehler, den Lektion 10.4 erklärt hat, an einer neuen Stelle. Kopfzeilen gehen vor
dem Rumpf über die Leitung. Sobald das erste echo gelaufen ist, sind sie weg, und der Status steht
fest. Dein Handler darf setzen, was er will: Die Antwort ist längst unterwegs, und zwar als 200.
Ein Handler, der zu spät kommt, macht die Sache sogar schlimmer. Vorher hattest du eine kaputte Seite. Jetzt hast du eine kaputte Seite mit einer beruhigenden Zeile darunter.
Der Puffer hält die Antwort zurück
<?php
// Zwei Zeilen Unterschied zum Beispiel davor: ob_start() ganz oben
// und ob_end_clean() als erste Zeile im Handler.
ob_start();
set_exception_handler(function (Throwable $fehler) {
ob_end_clean();
http_response_code(500);
error_log("[500] " . get_class($fehler) . ": " . $fehler->getMessage());
echo "<!doctype html><html lang=\"de\"><head><meta charset=\"utf-8\">";
echo "<title>Da ist etwas schiefgegangen</title></head><body>\n";
echo "<h1>Da ist etwas schiefgegangen</h1>\n";
echo "<p>Der Fehler ist notiert. Versuch es gleich noch einmal.</p>\n";
echo "</body></html>";
});
function preisVon(string $artikel): float
{
throw new RuntimeException("Kein Preis fuer " . $artikel);
}
echo "<!doctype html><html lang=\"de\"><head><meta charset=\"utf-8\">";
echo "<title>Preisliste</title></head><body>\n";
echo "<h1>Preisliste</h1>\n";
echo "<p>Kaffee: ", preisVon("kaffee"), " Euro</p>\n";
echo "</body></html>"; Die Lösung sind zwei Zeilen. ob_start() ganz oben schaltet einen Ausgabepuffer ein: Alles, was
danach mit echo geschrieben wird, sammelt PHP erst einmal im Speicher, statt es hinauszuschicken.
Damit ist auch noch keine Kopfzeile unterwegs.
Geht alles gut, schickt PHP den Puffer am Ende von selbst hinaus, und niemand merkt etwas davon. Geht
etwas schief, macht der Handler als Erstes ob_end_clean(). Das wirft den Puffer weg, ohne ihn
auszugeben, und damit ist die halbe Seite verschwunden, bevor sie jemand gesehen hat. Erst danach
setzt er den Status und schreibt seine eigene Seite.
Ruf das dritte Beispiel auf und sieh wieder in den Reiter Netzwerk: 500 Internal Server Error, und im Rumpf steht nur noch deine Fehlerseite. Von der Preisliste ist nichts übrig.
Was in die Antwort gehört und was ins Protokoll
Die Fehlerseite im Beispiel sagt nichts über den Fehler. Kein Klassenname, keine Meldung, keine Zeilennummer. Das ist Absicht und derselbe Gedanke wie in Lektion 15.5: Für dich ist eine Meldung ein Hinweis, für jemanden, der deine Anwendung auseinandernimmt, ist sie ein Bauplan.
Wissen willst du es trotzdem, und dafür steht die Zeile mit error_log() daneben. Sie schreibt auf
den Fehlerkanal, den du in Lektion 7.5 schon benutzt hast, und bei einem laufenden Server ist das
sein Protokoll: tail -3 /tmp/lern-web.log im Terminal zeigt nach dem dritten Beispiel die Zeile
[500] RuntimeException: Kein Preis fuer kaffee. Beim Prüfen der Aufgabe steht dasselbe Protokoll
im Terminal, und die letzte Prüfzeile sieht dort nach. Beide Seiten bekommen also, was sie brauchen:
der Besucher einen Satz, du die Meldung.
Auf deinem eigenen Rechner willst du das Gegenteil, nämlich die Meldung sehen. Dafür gibt es den
Schalter aus Lektion 16.5 bereits: Im Modus entwicklung darf der Handler die Meldung ausgeben, im
Modus betrieb bleibt es beim Satz. Die Auffangfunktion ist für beide dieselbe, nur ihr Inhalt
hängt an einem if.
Wo das Netz endet
<?php
// Dasselbe Netz, und diesmal geht der Speicher aus. Das ist kein
// Throwable, der Handler wird also gar nicht erst gerufen.
ob_start();
set_exception_handler(function (Throwable $fehler) {
ob_end_clean();
http_response_code(500);
echo "<h1>Da ist etwas schiefgegangen</h1>";
});
echo "<h1>Preisliste</h1>\n";
ini_set("memory_limit", "2M");
$riesig = str_repeat("Kaffee", 50000000);
echo "<p>Diese Zeile kommt nie an.</p>\n"; Ehrlichkeit gehört dazu: set_exception_handler() fängt alles, was ein Throwable ist, und das ist
in PHP 8 fast jeder Fehler. Auch ein Aufruf einer Funktion, die es nicht gibt, kommt dort an, als
Error.
Fast jeder heißt nicht jeder. Das vierte Beispiel lässt den Speicher ausgehen, und das ist kein
Throwable: Der Handler wird nicht gerufen, und im Reiter Netzwerk steht wieder 200. Was der
Besucher bekommt, ist nur noch die Meldung selbst; sogar die Überschrift aus dem Puffer ist weg,
denn PHP räumt bei diesem Abbruch nicht mehr auf, sondern hört auf. Für diesen Rest gibt es
register_shutdown_function(), und auch die kommt zu spät: Nachgemessen antwortet ein ob_end_clean()
darin mit Failed to delete buffer. No buffer to delete, und der Status lässt sich ebenfalls nicht
mehr setzen.
Das ist kein Grund, das Netz wegzulassen. Es ist der Grund, warum display_errors = Off auf einem
öffentlichen Server trotzdem gilt: Es ist die einzige Vorkehrung, die auch dann noch greift, wenn
kein Code von dir mehr läuft.
Was ganz oben stehen muss
Zusammengefasst braucht dein Front Controller drei Dinge, und alle drei stehen vor der ersten Ausgabe:
ob_start(), damit noch nichts hinausgeht.set_exception_handler(...), mitob_end_clean()als erster Zeile darin.- Darin
http_response_code(500), eine kurze Seite und einerror_log().
Danach kommt der Rest, den du schon hast: Konfiguration, Routen, Verteiler. Und alles, was dahinter schiefgeht, landet in deiner Auffangfunktion, egal in welcher Seitenfunktion es passiert ist.
Zum Mitnehmen
Dein Front Controller ist die eine Tür, durch die jede Anfrage geht. Damit ist er auch die eine Stelle, an der du auffangen kannst, woran du nicht gedacht hast. In Lektion 7.5 stand, dass du so etwas hier bauen wirst. Jetzt baust du es.
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.