PHP: Was bedeutet "Cannot modify header information"?

Warning: Cannot modify header information - headers already sent by
(output started at /var/www/html/datei.php:1) in /var/www/html/datei.php on line 3

Diese Meldung nennt dir die Ursache ziemlich genau. Man muss nur wissen, welche der beiden Zeilennummern darin die richtige ist — und die meisten lesen die falsche.

Warum das überhaupt passiert

HTTP schickt zuerst alle Header, dann eine Leerzeile, dann den Body. In dieser Reihenfolge, und sie lässt sich nicht umdrehen.

Sobald dein Skript also das erste Byte ausgibt, muss PHP die Header abschicken — sonst käme der Body vor ihnen an. Jeder spätere header()-Aufruf kommt zu spät.

“Ausgabe” ist dabei alles, was aus deinem Skript herausfällt. Auch ein einzelnes Leerzeichen.

Die Meldung enthält zwei Ortsangaben

Und das ist der Punkt, an dem die meisten Zeit verlieren:

headers already sent by (output started at DATEI:N) in DATEI on line M

N ist die Zeile, die die Ausgabe verursacht hat. M ist nur die Zeile, in der der header()-Aufruf steht, der sich beschwert.

Wer bei M nachsieht, findet dort einen ganz normalen header()-Aufruf und nichts Auffälliges. Die Ursache steht bei N.

Ich habe die vier häufigsten Fälle in einem Apache-Container nachgestellt. Hier sind sie mit den echten Zeilennummern.

Fall 1: Eine Leerzeile vor dem <?php

Die Datei beginnt mit einer leeren Zeile, dann kommt <?php.

(output started at /var/www/html/a-leerzeile-davor.php:1)
in /var/www/html/a-leerzeile-davor.php on line 3

Alles außerhalb von <?php ... ?> gibt PHP unverändert aus. Auch ein Zeilenumbruch.

Fall 2: Whitespace nach dem ?>

Die Datei hat in Zeile 3 ein ?> und danach eine Leerzeile.

(output started at /var/www/html/b-nach-schliessendem-tag.php:4)
in /var/www/html/b-nach-schliessendem-tag.php on line 6

Das ist der Fall, den man am schwersten sieht, weil viele Editoren am Dateiende automatisch einen Zeilenumbruch anhängen — und das ist auch richtig so, es gehört zur Definition einer Textzeile.

Die Lösung ist einfach: In Dateien, die nur PHP enthalten, lässt man das schließende ?> weg. Das ist keine Marotte, sondern die offizielle Empfehlung im PHP-Handbuch und in PSR-12. Wenn kein ?> da ist, kann danach auch nichts stehen.

Fall 3: Ein echo vor dem header()

(output started at /var/www/html/c-echo-davor.php:2)
in /var/www/html/c-echo-davor.php on line 3

Der offensichtliche Fall. Häufig ist es kein echo, sondern ein vergessenes var_dump() aus der Fehlersuche — oder eine PHP-Warnung, die selbst schon ausgegeben wurde und damit die Header abgeschickt hat.

Der zweite Punkt ist wichtig: Wenn display_errors an ist, ist jede Warnung eine Ausgabe. Ein “Undefined array key” weiter oben in deinem Skript kann also der Grund dafür sein, dass weiter unten die Weiterleitung nicht funktioniert.

Fall 4: Der BOM

(output started at /var/www/html/d-bom.php:1)
in /var/www/html/d-bom.php on line 2

Der Byte Order Mark ist eine unsichtbare Bytefolge (EF BB BF) am Dateianfang, die manche Editoren beim Speichern als UTF-8 hinzufügen. Für PHP ist er Ausgabe wie jede andere.

Das ist der Fall, der am längsten Rätsel aufgibt: Die Meldung zeigt auf Zeile 1, und in Zeile 1 steht sichtbar <?php. Im Body der Antwort sieht man den BOM dann als merkwürdiges Zeichen vor der eigentlichen Ausgabe.

Prüfen kannst du das so:

user@pc:~$ head -c 3 datei.php | xxd
00000000: efbb bf                                  ...

Wenn dort efbbbf steht, hast du einen BOM. Die meisten Editoren haben eine Einstellung “UTF-8 ohne BOM” — die ist für PHP-Dateien die richtige.

Wie du die Ursache findest

Der schnellste Weg ist headers_sent() mit zwei Parametern:

<?php
  if (headers_sent($datei, $zeile)) {
    echo "Ausgabe begann in $datei, Zeile $zeile";
  }
?>
Ausgabe hier in Zeile 2
headers_sent() : true
  Datei : f-headers-sent.php
  Zeile : 2

Damit bekommst du den Verursacher genannt, nicht den Beschwerdeführer. Das hilft besonders dann, wenn viele Dateien per include zusammenkommen und die Ausgabe aus einer ganz anderen stammt als der, in der du gerade bist.

Der Grund, warum es “nur auf dem Server” auftritt

Jetzt der Teil, der die Sache oft so verwirrend macht. PHP hat eine Einstellung namens output_buffering. Ist sie aktiv, sammelt PHP die Ausgabe erst einmal in einem Puffer, und die Header können auch noch nachträglich gesetzt werden.

Viele Distributionen und Docker-Images setzen einen Puffer von 4096 Byte. Auf so einem System tritt der Fehler bei kleinen Ausgaben gar nicht auf — bis die Ausgabe größer wird als der Puffer oder du auf ein System wechselst, das keinen hat.

Deshalb: Wenn der Fehler nur auf einem von zwei Systemen auftritt, vergleich zuerst

user@pc:~$ php -i | grep output_buffering

Für den Testaufbau zu diesem Artikel musste ich output_buffering = Off setzen, sonst hätte sich keiner der vier Fälle gezeigt.

Output Buffering als Notlösung

Man kann sich das auch zunutze machen:

<?php
  ob_start();
  echo "Diese Ausgabe wird gepuffert\n";
  header('X-Test: sieben');
  echo "und der Header geht trotzdem durch\n";
  ob_end_flush();
?>
H| X-Test: sieben
B| Diese Ausgabe wird gepuffert
B| und der Header geht trotzdem durch

Der Header kommt an, obwohl vorher echo stand.

Ich halte das für eine Notlösung und nicht für eine Lösung. Es versteckt das eigentliche Problem — dass dein Code Ausgabe erzeugt, bevor er weiß, was er ausgeben will. Und es kostet Speicher, weil die gesamte Antwort erst gesammelt wird.

Der saubere Weg ist, die Struktur zu ändern: erst rechnen und entscheiden, dann ausgeben. Alle header()-Aufrufe gehören an den Anfang, vor die erste Zeile Ausgabe.

Und wie es richtig aussieht

<?php
  header('X-Test: fuenf');
  header('Content-Type: text/plain');
  echo "Ausgabe\n";
?>
H| HTTP/1.1 200 OK
H| X-Test: fuenf
H| Content-Type: text/plain;charset=UTF-8
B| Ausgabe

Zusammenfassung

  • Die Meldung nennt zwei Orte. output started at DATEI:N ist die Ursache, on line M nur die Beschwerde.
  • headers_sent($datei, $zeile) liefert dir den Verursacher direkt.
  • Das schließende ?> in reinen PHP-Dateien weglassen — dann kann danach nichts mehr stehen.
  • Dateien ohne BOM speichern; prüfen mit head -c 3 datei.php | xxd.
  • Bei aktivem display_errors ist jede Warnung eine Ausgabe.
  • Tritt der Fehler nur auf einem System auf, vergleich output_buffering.
  • ob_start() verdeckt das Problem, es löst es nicht.

Hinweis zu Netcup (Werbung)

Der deutsche Hoster Netcup bietet unter anderem günstige und zugleich leistungsstarke Webhosting Pakete, KVM-basierte Root Server und dezidierte Server an. Mit unseren Gutscheincodes kannst du noch mehr Geld sparen (6€ bei deiner ersten Bestellung, 30% Rabatt auf alle KVM-basierten Root Server, ...).