mitmario.dev

Wenn doch etwas durchkommt

PHP Sandbox 5 Min Lesezeit 4 BeispieleLektion 6 von 8

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

Ein Fehler mitten in der Seite
<?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

Der Handler allein reicht nicht
<?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

Mit Puffer fliegt die halbe Seite weg
<?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

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:

  1. ob_start(), damit noch nichts hinausgeht.
  2. set_exception_handler(...), mit ob_end_clean() als erster Zeile darin.
  3. Darin http_response_code(500), eine kurze Seite und ein error_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, kostenlos

Zu 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.