Webentwicklung

Fehlerbehandlung in kleinen PHP-Produktivanwendungen - Weniger zeigen, mehr erfahren

Fehlerbehandlung in kleinen PHP-Produktivanwendungen - Weniger zeigen, mehr erfahren

Eine PHP-Seite scheitert mitten in der Verarbeitung einer Anfrage. Was sollte der Besucher erhalten, und was sollte der Betreiber aufbewahren? Diese Fragen hängen zusammen, sollten aber nicht dieselbe Antwort haben. Ein Besucher benötigt eine stabile Antwort, die keine Interna der Anwendung offenlegt. Die Person, die den Fehler untersucht, benötigt genügend private Hinweise, um den fehlerhaften Pfad zu finden.

Diese Trennung wird bei kleinen Anwendungen leicht übersehen. Jeden Fehler anzuzeigen, ist während der Entwicklung bequem, legt in der Produktion jedoch Details offen. Alle Fehler auszublenden, ohne sie aufzuzeichnen, erzeugt das gegenteilige Problem: eine höfliche leere Wand ohne brauchbare Spur dahinter. Ein belastbareres Design erzeugt aus einem unerwarteten Fehler zwei Ausgaben: eine zurückhaltende HTTP-Antwort und ein geschütztes Diagnoseereignis.

Dieser Artikel entwickelt dieses Muster für eine kleine PHP-Anwendung. Es ist bewusst eine Grundlage und keine vollständige Monitoring-Plattform. Frameworks bieten möglicherweise bereits einen besser integrierten Handler, und jedes Deployment benötigt weiterhin eigene Entscheidungen zu Aufbewahrung, Zugriffskontrolle und Alarmierung.

Zuerst erwartete und unerwartete Fehler trennen

Nicht jede erfolglose Anfrage ist ein Absturz der Anwendung. Ein fehlender Artikel, ein ungültiges Formularfeld, eine abgelaufene Anmeldung und eine fehlgeschlagene Autorisierungsprüfung sind Ergebnisse, die eine Anwendung vorhersehen kann. Sie sollten nahe am betroffenen Code ausdrücklich behandelt werden, gegebenenfalls einen passenden 4xx-Status erhalten und dem Benutzer mit einer Meldung den nächsten sinnvollen Schritt erklären.

Ein unerwarteter Fehler ist etwas anderes. Ein Programmierfehler, eine verletzte Invariante oder eine nicht verfügbare Abhängigkeit kann den Server daran hindern, eine gültige Anfrage abzuschließen. RFC 9110 definiert 5xx-Antworten als Serverfehler und beschreibt 500 als unerwarteten Zustand, der die Erfüllung der Anfrage verhindert hat. Damit eignet sich 500 als letzter Ausweg für einen nicht abgefangenen serverseitigen Fehler. Der Status sollte nicht zum bequemen Sammelbecken für jede abgelehnte Eingabe werden.

Diese Unterscheidung ist für den Betrieb wichtig. Wenn gewöhnliche Validierungsfehler zu 500-Antworten werden, sind echte Vorfälle im Rauschen schwerer zu erkennen. Wenn tatsächliche Abstürze 200 zurückgeben, können Clients, Uptime-Prüfungen und Zugriffsprotokolle einen Fehler für einen Erfolg halten. Der Statuscode ist Teil der Diagnose und keine Dekoration der Fehlerseite.

PHP zum Berichten ohne Offenlegung konfigurieren

Die Fehlerbehandlung in der Produktion beginnt unterhalb des Anwendungshandlers. Die PHP-Dokumentation zur Laufzeitkonfiguration erklärt, dass display_errors eine Entwicklungsfunktion ist und nicht auf Produktionssystemen verwendet werden sollte. Unabhängig davon empfiehlt sie für produktive Websites das Protokollieren statt der Anzeige von Fehlern.

Eine zurückhaltende Produktionskonfiguration sieht so aus:

; production php.ini or an appropriate PHP-FPM configuration
error_reporting = E_ALL
display_errors = Off
display_startup_errors = Off
log_errors = On

error_reporting = E_ALL bedeutet nicht, dass jede Diagnose im Browser erscheinen muss. Erfassen, Anzeigen und Protokollieren sind getrennte Einstellungen. Eine umfassende Erfassung bei deaktivierter Anzeige ermöglicht es, Diagnosen zum konfigurierten Protokollziel zu leiten, ohne die Antwort in eine Debugging-Ansicht zu verwandeln.

Das genaue Ziel hängt vom Deployment ab. PHP kann je nach Konfiguration ein ausdrücklich festgelegtes error_log, den SAPI-Logger oder den System-Logger verwenden. Dieses Ziel benötigt eingeschränkten Lesezugriff, funktionierende Schreibrechte, Rotation und ausreichend freien Speicher. Eine aktivierte Logging-Einstellung ist noch kein Beleg dafür, dass Einträge tatsächlich dauerhaft gespeichert werden.

Diese Einstellungen gehören in die Serverkonfiguration und nicht ausschließlich in den Anwendungscode. PHP weist darauf hin, dass eine Laufzeitänderung von display_errors keinen fatalen Fehler beeinflussen kann, der auftritt, bevor der entsprechende Aufruf ausgeführt wird. Ein Syntaxfehler in der Bootstrap-Datei lässt sich nicht durch eine ini_set()-Zeile beheben, die PHP nie erreicht.

Einen kleinen Handler als letzte Grenze hinzufügen

PHPs set_exception_handler() registriert einen Callback für ein nicht abgefangenes Throwable. Das Throwable-Interface umfasst sowohl Error als auch Exception. Nach dem Callback endet die Ausführung; dies ist eine Grenze für einen geordneten Abbruch und keine Möglichkeit, die fehlgeschlagene Anfrage fortzusetzen.

Das folgende frameworkunabhängige Beispiel erzeugt eine Korrelations-ID, bevor es einen minimalen Handler registriert. Es protokolliert zurückhaltend ausgewählte Felder, setzt einen ausdrücklichen Status und gibt eine generische HTML-Antwort zurück:

<?php
declare(strict_types=1);

$requestId = bin2hex(random_bytes(8));

set_exception_handler(static function (Throwable $error) use ($requestId): void {
    $event = [
        'time' => gmdate('c'),
        'level' => 'error',
        'event' => 'uncaught_throwable',
        'request_id' => $requestId,
        'type' => $error::class,
        'file' => $error->getFile(),
        'line' => $error->getLine(),
    ];

    error_log((string) json_encode(
        $event,
        JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE
    ));

    if (!headers_sent()) {
        http_response_code(500);
        header('Content-Type: text/html; charset=UTF-8');
        header('Cache-Control: no-store');
    }

    $safeId = htmlspecialchars($requestId, ENT_QUOTES, 'UTF-8');
    echo '<h1>Something went wrong</h1>';
    echo '<p>Please try again later. Reference: ' . $safeId . '</p>';
});

Die öffentliche Referenz und die private request_id erlauben es einem Betreiber, das zugehörige Ereignis zu finden, wenn ein Besucher ein Problem meldet. Wie das OWASP Logging Cheat Sheet erläutert, hilft ein Interaktionsbezeichner dabei, Ereignisse derselben Interaktion zu verbinden. Er ist nur ein Korrelationswert: keine Authentifizierung, keine Autorisierung und kein Geheimnis.

Der ausdrückliche Aufruf von http_response_code(500) ist wichtig, weil PHP im Webserver-Kontext standardmäßig den Status 200 verwendet, bis ein anderer gesetzt wird. Die Prüfung mit headers_sent() erkennt eine Grenze an, statt sie zu verbergen. Wenn die Ausgabe bereits begonnen hat, kann der Handler Status und Header möglicherweise nicht mehr ersetzen. Output Buffering und ein Front Controller können dieses Risiko verringern, doch die richtige Wahl hängt von der Anwendung ab.

Genug protokollieren, aber nicht alles

Es kann verlockend sein, die vollständige Exception-Meldung, den Stack Trace, Request Body, Header, Session und Benutzerdatensatz hinzuzufügen. Mehr Kontext kann die Fehlersuche verkürzen, aber wahlloser Kontext erzeugt eine zweite sensible Datenbank, die als Textdatei getarnt ist.

OWASP empfiehlt, Passwörter, Access Tokens, Session-IDs, Datenbank-Verbindungszeichenfolgen, kryptografische Schlüssel und sensible personenbezogene Daten normalerweise nicht direkt zu protokollieren. Exception-Meldungen können ebenfalls SQL-Text, Dateisystempfade, entfernte Antworten oder benutzergesteuerte Werte enthalten. Das Beispiel beginnt deshalb mit Ereignistyp, Klasse, Codeposition, Zeit und Korrelations-ID. Eine Anwendung kann sorgfältig ausgewählte Felder ergänzen, sobald deren Datenherkunft und Regeln zur Schwärzung verstanden sind.

Nicht vertrauenswürdiger Text beeinflusst auch die Integrität von Logs. Wagenrückläufe, Zeilenvorschübe und Trennzeichen können Einträge fälschen oder einen nachgelagerten Parser stören. Strukturierte JSON-Codierung ist vorhersehbarer als manuell zusammengesetzte Zeichenfolgen mit Trennzeichen, doch die Codierung allein entscheidet nicht, ob ein Feld sicher oder notwendig ist. Feldlängen sollten validiert, unzulässige Steuerzeichen gegebenenfalls entfernt und rohe Request-Daten standardmäßig ausgeschlossen werden.

Der Zugriff auf Logs verdient dieselbe Sorgfalt wie andere privilegierte Anwendungsdaten. Logs sollten außerhalb des öffentlichen Document Root gespeichert, Leser beschränkt, Dateien rotiert und eine Aufbewahrungsdauer anhand tatsächlicher betrieblicher und rechtlicher Anforderungen festgelegt werden. Es gibt keine universelle Dauer, die zu jeder persönlichen Website oder Organisation passt.

Was der minimale Handler nicht löst

Ein Handler als letzte Grenze verbessert den abschließenden Fehlerpfad, doch mehrere Grenzen bleiben:

  • Er behandelt keine erwarteten fachlichen Ergebnisse; diese benötigen weiterhin lokale und spezifische Antworten.
  • Er kann eine bereits teilweise gesendete Antwort nicht schützen und nicht laufen, bevor der Bootstrap seine Registrierung erreicht.
  • Er beweist nicht, dass PHP-FPM, der Webserver oder das Betriebssystem den Logeintrag gespeichert hat.
  • Er erkennt weder einen hängenden Worker noch einen beendeten Prozess, eine volle Festplatte oder einen Server, der PHP nie aufruft.
  • Er sollte einfach bleiben, weil ein komplizierter Handler selbst scheitern kann, während sich die Anwendung bereits in einem unsicheren Zustand befindet.

Das OWASP Error Handling Cheat Sheet beschreibt das Ziel treffend: Bei einem unerwarteten Fehler wird eine generische Antwort zurückgegeben, während Details serverseitig zur Untersuchung protokolliert werden. Das verringert unbeabsichtigte Informationslecks, ersetzt aber weder die Behebung des eigentlichen Defekts noch Eingabevalidierung, Autorisierung oder Verfügbarkeitsüberwachung.

Den Fehlerpfad bewusst prüfen

Ein nie ausgeführter Fehlerpfad bleibt eine Annahme. Er sollte in einer kontrollierten Nicht-Produktionsumgebung getestet und die Produktionskonfiguration anschließend ohne Offenlegung sensibler Ausgaben geprüft werden. Sinnvolle Kontrollen sind:

  1. Nach der Registrierung des Handlers eine bekannte Test-Exception auslösen.
  2. Bestätigen, dass der Antwortstatus 500 statt 200 ist.
  3. Bestätigen, dass der Body nur die generische Meldung und Korrelations-ID enthält, ohne Stack Trace, Pfad, SQL oder Geheimnis.
  4. Dieselbe ID im vorgesehenen serverseitigen Log finden und prüfen, ob das Ereignis lesbar ist.
  5. Einen gewöhnlichen Validierungsfehler getrennt testen und bestätigen, dass er seine vorgesehene 4xx-Antwort behält.
  6. Fehlende Log-Schreibrechte oder knappen Speicher in einer sicheren Umgebung simulieren und das Verhalten beobachten.
  7. Rotation, Aufbewahrung und Zugriffsrechte prüfen, statt darauf zu warten, dass die Festplatte oder ein unbefugter Leser das Versäumnis offenlegt.

Das Logging Cheat Sheet empfiehlt ausdrücklich, Fehler des Logging-Systems, Ressourcenerschöpfung, Widerstand gegen Injection und Zugriffskontrollen zu testen. Dies ist die weniger auffällige Hälfte der Fehlerbehandlung, entscheidet aber darüber, ob die Belege vorhanden sind, wenn sie gebraucht werden.

Öffentlich weniger zeigen, privat mehr erfahren

Gute Fehlerbehandlung in der Produktion bedeutet nicht Schweigen. Sie trennt bewusst zwei Zielgruppen. Der Besucher erhält einen ehrlichen Status, eine ruhige Meldung und eine Referenz, die er nennen kann. Der Betreiber erhält ein begrenztes Ereignis an einem geschützten Ort. Erwartete Fehler bleiben spezifisch; unerwartete Fehler erreichen eine kleine letzte Grenze.

Das minimale Muster hier ist ein Ausgangspunkt. Eine reale Anwendung benötigt möglicherweise Framework-Integration, zentrale Sammlung, Alarme, Traces oder stärkere Schwärzung. Die hilfreiche Frage lautet nicht: „Wie viele Fehlerdetails lassen sich erfassen?“, sondern: „Welche minimalen Details machen diesen Fehler verständlich, ohne ein weiteres Offenlegungsrisiko zu schaffen?“

References