HTTP-Header steuern die Kommunikation zwischen Browser und Server. Oft unsichtbar, aber für jede Webanwendung unverzichtbar. Sie geben Anweisungen zur Darstellung, Weiterleitung, Caching und Sicherheit.
Mit der PHP-Funktion header() kannst du diese Header flexibel beeinflussen. Richtig eingesetzt, macht das den Unterschied zwischen einer professionellen, robusten Anwendung und fehleranfälligem Spaghetti-Code.

In diesem Guide lernst du praxisnah, wie du mit header() arbeitest, Fehler vermeidest, Sicherheit berücksichtigst und typische Use-Cases sauber umsetzt.
Überblick: So funktioniert der HTTP-Header-Ablauf
Bevor du mit header() arbeitest, hilft es zu verstehen, wie HTTP-Header in den Kommunikationsablauf zwischen Browser und Server eingebettet sind. Das folgende Diagramm zeigt den typischen Ablauf:
sequenceDiagram
participant B as Browser
participant S as Server (PHP)
B->>S: HTTP-Request (GET /seite.php)
Note over S: PHP startet
Note over S: header() setzt Header
Note over S: echo erzeugt Body
S->>B: HTTP-Response (Header + Body)
Note over B: Browser wertet Header aus
Note over B: z.B. Redirect, Caching, Content-Type
Entscheidend ist: PHP muss alle Header setzen, bevor der Body (also die HTML-Ausgabe) beginnt. Sobald echo, print oder auch nur ein Leerzeichen vor <?php eine Ausgabe erzeugt, sind die Header bereits gesendet und können nicht mehr geändert werden.
Die Funktion header() ermöglicht es dir, direkt aus PHP heraus HTTP-Header an den Browser zu senden. So steuerst du das Verhalten von Clients, gibst Statuscodes aus oder löst Weiterleitungen und Datei-Downloads aus.
Mit header() sendest du einen oder mehrere rohe HTTP-Header an den Browser, bevor der eigentliche Seiteninhalt (Body) kommt. Das ist entscheidend, denn HTTP sieht erst die Header, dann den Body vor.
Typische Anwendungsfälle sind Weiterleitungen, Dateidownloads, das Setzen des Content-Type oder Steuerung des Cachings.
Du musst header() immer aufrufen, bevor irgendetwas ausgegeben wird: kein HTML, keine Leerzeichen, kein Echo, kein UTF-8 BOM.
Sonst kommt es zum berühmten Fehler:
Warning: Cannot modify header information - headers already sent by...
Syntax und Parameter
header(string $sHeader, bool $bErsetzen = true, int $iStatuscode = 0);
- $sHeader: Der eigentliche Header-String, z.B. "Location: /startseite".
- $bErsetzen: Standard ist true, d.h. vorhandene gleichnamige Header werden überschrieben. Setze auf false, um mehrere gleiche Header zu senden (z.B. bei Set-Cookie).
- $iStatuscode: Optional. Damit setzt du den HTTP-Status explizit.
Spezialfälle
-
Status-Code setzen:
header("HTTP/1.1 404 Not Found");
-
Location-Header (Weiterleitung):
header("Location: https://neue-seite.de");
Rückgabewert:
Die Funktion gibt keinen Wert zurück (void).
Wichtige Versionen:
Seit PHP 5.1.2 werden Header Injection Versuche verhindert (doppelte Header-Zeilen im String führen zu Fehlern).
Mit gezielten HTTP-Headern kannst du Benutzer oder Suchmaschinen an eine andere URL weiterleiten. Das ist ideal für Umzüge, Login-Weiterleitungen oder das Vermeiden doppelter Inhalte.
1. URL-Weiterleitungen (Redirects)
Einfache Weiterleitung
header("Location: https://neue-seite.de");
/** Nach Location immer Skript beenden! */
exit;
301 Moved Permanently (dauerhafte Weiterleitung, SEO-freundlich)
header("HTTP/1.1 301 Moved Permanently");
header("Location: /neuer-pfad");
exit;
307 Temporary Redirect (temporär, Formulardaten bleiben erhalten)
header("HTTP/1.1 307 Temporary Redirect");
header("Location: /wartung");
exit;
Weiterleitung mit Verzögerung (Refresh), meist nicht empfohlen
header("Refresh: 5; URL=https://neue-seite.de");
/** Besser Location-Header verwenden, da Refresh für SEO und Usability problematisch ist. */
2. Content-Type deklarieren
Mit dem passenden Content-Type Header teilst du dem Browser mit, um welche Art von Inhalt es sich handelt. Das stellt sicher, dass Texte, Bilder oder Dateien korrekt interpretiert und dargestellt werden.
Warum wichtig?
Der Content-Type sagt dem Browser, wie der Inhalt zu behandeln ist.
Beispiele:
header("Content-Type: text/html; charset=utf-8"); /** Für HTML */
header("Content-Type: application/json"); /** Für API-Ausgaben */
header("Content-Type: application/pdf"); /** Für PDF-Downloads */
header("Content-Type: text/css"); /** Für CSS-Dateien */
header("Content-Type: application/javascript"); /** Für JS-Dateien */
3. Caching-Verhalten steuern
Du kannst das Caching gezielt steuern oder komplett unterbinden, indem du entsprechende Header setzt. So bestimmst du, ob und wie lange Inhalte im Browser oder Proxy gespeichert werden dürfen.
Caching verhindern:
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Cache-Control: post-check=0, pre-check=0", false); /** Für ältere IE-Versionen */
header("Pragma: no-cache"); /** HTTP/1.0-Kompatibilität */
header("Expires: 0"); /** Oder ein Datum in der Vergangenheit */
Caching erlauben (z. B. 1 Stunde):
header("Cache-Control: public, max-age=3600");
header("Expires: " . gmdate("D, d M Y H:i:s", time() + 3600) . " GMT");
4. Datei-Downloads erzwingen
Durch die richtigen HTTP-Header lässt sich ein Dateidownload im Browser auslösen, sodass beispielsweise PDFs nicht direkt angezeigt, sondern zum Herunterladen angeboten werden.
PDF-Download mit Download-Dialog:
header("Content-Type: application/pdf");
header("Content-Disposition: attachment; filename=\"mein-dokument.pdf\"");
header("Content-Length: " . filesize("pfad/zur/datei.pdf"));
readfile("pfad/zur/datei.pdf");
exit;
Praxisbeispiel: CSV-Export für Downloads
Neben PDF-Downloads ist der CSV-Export einer der häufigsten Anwendungsfälle in Business-Anwendungen. Berichte, Nutzerlisten oder Bestellungen lassen sich damit einfach als Tabelle herunterladen. Wichtig ist dabei das UTF-8 BOM am Anfang, damit Excel die Umlaute korrekt darstellt:
<?php
header("Content-Type: text/csv; charset=utf-8");
header("Content-Disposition: attachment; filename="export.csv"");
/* php://output schreibt direkt in den Response */
$fp = fopen("php://output", "w");
/* UTF-8 BOM für Excel-Kompatibilität */
fwrite($fp, "\xEF\xBB\xBF");
/* Kopfzeile */
fputcsv($fp, ["Name", "E-Mail", "Datum"]);
/* Datenzeilen */
$daten = [
["Max Mustermann", "max@example.com", "2026-01-15"],
["Erika Muster", "erika@example.com", "2026-02-20"],
];
foreach ($daten as $zeile) {
fputcsv($fp, $zeile);
}
fclose($fp);
exit;
?>
Der Trick mit php://output und fputcsv() sorgt dafür, dass du keinen Zwischenspeicher brauchst. Die Daten werden direkt in den HTTP-Response geschrieben. Das funktioniert auch bei großen Datenmengen speicherschonend.
5. HTTP-Statuscodes setzen
Das gezielte Setzen von HTTP-Statuscodes informiert Client und Suchmaschinen klar über den Zustand einer Anfrage. So kannst du Fehler, Weiterleitungen oder erfolgreiche Aufrufe eindeutig kennzeichnen.
Gängige Statuscodes:
- 200 OK
- 201 Created
- 204 No Content
- 400 Bad Request
- 401 Unauthorized
- 403 Forbidden
- 404 Not Found
- 500 Internal Server Error
- 503 Service Unavailable
Status setzen (zwei Wege):
header("HTTP/1.1 404 Not Found"); /** Über header() */
http_response_code(404); /** Bequeme Alternative seit PHP 5.4+ */
Tipp: http_response_code() statt header()
Seit PHP 5.4 gibt es die Funktion http_response_code(), die den Statuscode direkt über die SAPI-Schnittstelle setzt. Das ist zuverlässiger als header("HTTP/1.1 404 ..."), weil der Server bei FastCGI/FPM die Protokollversion selbst bestimmt. Außerdem ist der Code lesbarer:
<?php
/* Empfohlen: http_response_code() */
http_response_code(404);
/* Veraltet: header() mit Protokoll-String */
header("HTTP/1.1 404 Not Found");
/* Statuscode abfragen */
$code = http_response_code(); // 404
?>
http_response_code() wirkt aber nicht, wenn schon ein expliziter Header gesetzt wurde.
Der Fehler „Headers already sent“ tritt auf, wenn bereits eine Ausgabe erfolgt ist, bevor PHP versucht, HTTP-Header zu senden. Selbst unsichtbare Leerzeichen oder Zeilenumbrüche außerhalb der PHP-Tags können diesen Fehler auslösen.
Warum tritt dieser Fehler auf?
Header müssen vor jeglicher Ausgabe (auch Whitespace) an den Client gesendet werden.
Typische Ursachen:
- Leerzeichen oder Zeilenumbrüche vor <?php oder nach ?>
- Ausgabe durch echo, print etc. vor dem header-Aufruf
- HTML vor header()
- UTF-8 BOM am Dateianfang
- Fehlerausgaben von PHP
Häufige Fehlerquellen:
- Ungewollte Leerzeilen, z. B. nach schließenden PHP-Tags
- Editor speichert Datei mit BOM
- Include/Require-Dateien enthalten Ausgabe
- Fehlermeldungen erscheinen vor header()
Debugging:
Mit headers_sent() kannst du prüfen, ob bereits Header gesendet wurden:
if (headers_sent($sDatei, $iZeile)) {
echo "Fehler: Header bereits gesendet in $sDatei, Zeile $iZeile";
} else {
header("Location: /startseite");
exit;
}
Output Buffering als Notlösung:
Mit Output Buffering (ob_start()) lässt sich Ausgabe puffern und header() kann noch gesetzt werden.
Das ist aber nur ein Workaround, saubere Code-Struktur ist besser.
ob_start();
/** ...Code, der evtl. schon Ausgabe erzeugt... */
if ($bBedingung) {
header("Location: andere-seite.php");
ob_end_flush();
exit;
}
ob_end_flush();
Bei unsachgemäßer Behandlung von Benutzereingaben besteht die Gefahr sogenannter Header-Injection-Angriffe, die Manipulationen am HTTP-Header ermöglichen und erhebliche Sicherheitsrisiken mit sich bringen.
Bei einer Header Injection versucht ein Angreifer, über Benutzereingaben zusätzliche HTTP-Header einzuschleusen. Das funktioniert, indem Zeilenumbruch-Zeichen (\r\n, also Carriage Return + Line Feed) in den Header-Wert eingeschleust werden. Ein Zeilenumbruch im HTTP-Protokoll trennt Header voneinander, sodass nach dem Einschleusen ein völlig neuer Header entstehen kann.
Seit PHP 5.1.2 erkennt PHP solche Angriffe automatisch und blockiert Header-Strings, die Zeilenumbrüche enthalten. Trotzdem solltest du Benutzereingaben niemals direkt in Header übernehmen, sondern immer validieren:
<?php
/* FALSCH: Benutzereingabe direkt im Header */
header("Location: " . $_GET["url"]);
/* RICHTIG: Eingabe validieren */
$erlaubteSeiten = [
"/startseite",
"/profil",
"/kontakt",
];
$ziel = $_GET["url"] ?? "/startseite";
if (in_array($ziel, $erlaubteSeiten, true)) {
header("Location: " . $ziel);
exit;
}
http_response_code(400);
echo "Ungültiges Weiterleitungsziel.";
?>
Open Redirect Vulnerabilities
Bei dynamischen Weiterleitungen können Angreifer Nutzer auf externe Phishing-Seiten umleiten.
Absichern:
- Whitelisting
- Nur relative Pfade
- Indirekte Weiterleitungen über Schlüssel/IDs
Neben der Absicherung gegen Header Injection gibt es eine Reihe von HTTP-Headern, die deine Webseite zusätzlich schützen. Im Idealfall setzt du alle davon gemeinsam, zum Beispiel in einer zentralen Funktion:
<?php
function setSecurityHeaders(): void
{
/* Content-Security-Policy (CSP)
Bestimmt, von wo Ressourcen geladen
werden dürfen */
header(
"Content-Security-Policy: "
. "default-src \"self\"; "
. "script-src \"self\"; "
. "style-src \"self\" \"unsafe-inline\"; "
. "img-src \"self\" data:"
);
/* Strict-Transport-Security (HSTS)
Erzwingt HTTPS für 1 Jahr */
header(
"Strict-Transport-Security: "
. "max-age=31536000; includeSubDomains"
);
/* Clickjacking-Schutz */
header("X-Frame-Options: SAMEORIGIN");
/* MIME-Sniffing verhindern */
header("X-Content-Type-Options: nosniff");
/* Referrer einschränken */
header("Referrer-Policy: strict-origin-when-cross-origin");
/* Browser-Features einschränken */
header("Permissions-Policy: camera=(), microphone=(), geolocation=()");
}
/* Am Anfang jedes Requests aufrufen */
setSecurityHeaders();
?>
Content-Security-Policy (CSP) legt fest, von welchen Quellen der Browser Skripte, Styles und Bilder laden darf. Die Direktive default-src 'self' erlaubt nur Ressourcen von der eigenen Domain. Mit script-src und style-src kannst du das für einzelne Ressourcentypen gezielt anpassen.
HSTS teilt dem Browser mit, dass deine Seite ausschließlich per HTTPS erreichbar sein soll. Der Wert max-age gibt die Gültigkeit in Sekunden an. Mit includeSubDomains gilt die Regel auch für alle Subdomains.
X-Frame-Options verhindert, dass deine Seite in einem <iframe> auf einer fremden Domain eingebettet wird (Clickjacking-Schutz). SAMEORIGIN erlaubt Einbettung nur auf der eigenen Domain, DENY verbietet sie komplett.
X-Content-Type-Options: nosniff verhindert, dass der Browser den MIME-Type einer Datei selbst erraten versucht. Das schützt davor, dass eine hochgeladene Textdatei als JavaScript interpretiert wird.
Teil 5: Fortgeschrittene Techniken und verwandte Funktionen
In manchen Situationen ist es nötig, denselben Header-Typ mehrfach zu senden, etwa bei der Übergabe von mehreren Cookies oder bei Headern wie „Set-Cookie“ und „Cache-Control“.
Mit $bErsetzen = false kannst du z. B. mehrere Set-Cookie-Header senden.
header("Set-Cookie: cookie1=test1", false);
header("Set-Cookie: cookie2=test2", false);
HTTP-Antwortcode gezielt setzen
Siehe Unterschied header(“HTTP/1.1 …”) vs. http_response_code().
Letzteres ist kompakter, aber etwas weniger flexibel.
Siehe oben:
if (headers_sent()) { /* ... */ }
$aHeader = headers_list();
/** Ausgabe zu Debugzwecken */
foreach ($aHeader as $sHeader) {
echo $sHeader . "<br>";
}
header_remove("X-Powered-By");
Ohne Parameter entfernt alle Header, die durch PHP gesetzt wurden.
Wenn dein PHP-Backend als API dient und ein JavaScript-Frontend auf einer anderen Domain darauf zugreift, blockiert der Browser die Anfrage standardmäßig. Mit CORS-Headern erlaubst du gezielt den Zugriff.
Ein einfacher CORS-Header für eine bestimmte Domain sieht so aus:
<?php
/* Einfacher CORS-Header */
header("Access-Control-Allow-Origin: https://meine-app.de");
/* Preflight-Request behandeln (OPTIONS) */
if ($_SERVER["REQUEST_METHOD"] === "OPTIONS") {
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE");
header("Access-Control-Allow-Headers: Content-Type, Authorization");
header("Access-Control-Max-Age: 86400"); // 24h Cache
http_response_code(204);
exit;
}
/* Mit Credentials (Cookies/Auth) */
header("Access-Control-Allow-Credentials: true");
?>
Wichtig zu wissen: Sobald du Access-Control-Allow-Credentials: true setzt, darfst du bei Access-Control-Allow-Origin nicht mehr den Wildcard * verwenden. Stattdessen muss die exakte Origin-Domain stehen.
Der Browser sendet bei bestimmten Anfragen (z.B. mit Content-Type: application/json oder eigenen Headern) automatisch einen sogenannten Preflight-Request mit der HTTP-Methode OPTIONS. Dein Server muss diesen beantworten, bevor der eigentliche Request durchgeht. Der Parameter Access-Control-Max-Age teilt dem Browser mit, wie lange er das Preflight-Ergebnis cachen darf.
Mit diesen Best Practices stellst du sicher, dass deine HTTP-Header nicht nur korrekt, sondern auch sicher, übersichtlich und zuverlässig verarbeitet werden, unabhängig von Anwendungsfall oder Umgebung.
- Header-Logik immer ganz am Anfang des Skripts, vor jeglicher Ausgabe.
- Nach jeder Weiterleitung immer exit; oder die(); aufrufen.
- Validiere und sanitiere alle Werte, die aus Benutzereingaben stammen, bevor sie in Header eingefügt werden.
- Setze klare, passende Statuscodes, das hilft Debugging und SEO.
- Nutze Output Buffering nur bewusst, nicht als Ausrede für unstrukturierten Code.
- Sende nur benötigte Header, alles andere erhöht die Angriffsfläche und sollte daher manuell angegeben werden.
- Teste Header-Verhalten in verschiedenen Browsern und mit Tools wie curl oder den Browser-Dev-Tools.
Interaktiver Bereich / Schnelle Referenz
Im folgenden Abschnitt findest du häufige Stolperfallen bei der Arbeit mit HTTP-Headern in PHP sowie praxisnahe Lösungswege für schnellen Überblick und effektive Fehlersuche.
Typische Fehlerquellen und Lösungen
Problem: "Headers already sent"
- Stelle sicher, dass sich keine Leer- oder Steuerzeichen vor oder nach den PHP-Tags befinden.
- Gib keine Daten (auch kein Whitespace) vor dem Aufruf von header() aus.
- Achte darauf, dass dein Editor keine Byte Order Mark (BOM) speichert.
- Prüfe mit headers_sent(), ob schon Header verschickt wurden.
Problem: Weiterleitung schlägt fehl
- Kontrolliere, ob bereits eine Ausgabe stattgefunden hat.
- Vergiss nicht, nach dem Aufruf von header('Location: ...') ein exit; zu setzen.
Problem: Datei-Download startet nicht oder falscher Typ
- Überprüfe den korrekten Content-Type Header.
- Setze einen passenden Content-Disposition Header für Downloads.
- Gib die tatsächliche Content-Length an.
Problem: Caching greift nicht wie erwartet
- Setze sämtliche relevanten Header (Cache-Control, Pragma, Expires).
- Prüfe die Header mit den Entwicklertools deines Browsers.
Cheat Sheet
| Anwendungsfall | Beispiel-Code |
| Weiterleitung | header("Location: /ziel"); exit; |
| 301-Redirect | header("HTTP/1.1 301 Moved Permanently"); header("Location: /neu"); exit; |
| Content-Type setzen | header("Content-Type: application/json"); |
| Caching verbieten | header("Cache-Control: no-cache, must-revalidate"); |
| Datei-Download | header("Content-Disposition: attachment; filename="d.pdf""); |
| Header-Fehler debuggen | if (headers_sent($sDatei, $iZeile)) { echo "..."; } |
| Header entfernen | header_remove("X-Powered-By"); |
| CORS erlauben | header("Access-Control-Allow-Origin: *"); |
Häufige Fragen zu PHP header()
Die folgenden Fragen tauchen in der Praxis besonders oft auf.
Kann ich Header nach einer echo-Ausgabe setzen?
Nein, nicht direkt. Sobald PHP eine Ausgabe an den Browser schickt, werden die Header automatisch mit gesendet. Danach lässt sich nichts mehr ändern. Eine Möglichkeit ist der Einsatz von ob_start() am Scriptanfang. Damit puffert PHP die Ausgabe, und du kannst Header auch nach echo noch setzen. Der Puffer wird erst am Ende des Scripts oder beim Aufruf von ob_end_flush() tatsächlich gesendet.
Was ist der Unterschied zwischen 301 und 302 Redirect?
Ein 301 (Moved Permanently) signalisiert Suchmaschinen, dass die alte URL dauerhaft ersetzt wurde. Der SEO-Wert wird auf die neue URL übertragen. Ein 302 (Found) ist eine temporäre Weiterleitung. Browser und Suchmaschinen behalten die alte URL im Index. Verwende 301, wenn sich eine URL endgültig ändert, und 302 für kurzfristige Umleitungen, zum Beispiel während Wartungsarbeiten.
Wie teste ich HTTP-Header lokal?
Am einfachsten mit curl -I auf der Kommandozeile. Der Befehl curl -I http://localhost/seite.php zeigt nur die Response-Header an, ohne den Body zu laden. Alternativ kannst du im Browser die DevTools öffnen (F12), zum Tab "Netzwerk" wechseln und dort die Header jeder Anfrage inspizieren.
Funktioniert header() im CLI-Modus?
Technisch ja, PHP wirft keinen Fehler. Aber im CLI-Modus gibt es keinen Browser und keinen Webserver, der die Header interpretiert. Die Header-Aufrufe werden einfach ignoriert. Wenn du ein Script hast, das sowohl im Web als auch in der Kommandozeile laufen soll, prüfe mit php_sapi_name() !== 'cli', ob du dich in einer Webumgebung befindest.
Wie setze ich mehrere Cookies gleichzeitig?
Bei header() musst du den zweiten Parameter auf false setzen, damit der vorherige Set-Cookie-Header nicht überschrieben wird: header("Set-Cookie: name=wert", false). In der Praxis ist es allerdings einfacher, die Funktion setcookie() zu verwenden, die das automatisch korrekt handhabt.
Fazit
PHP header() ist ein mächtiges Werkzeug, das dir volle Kontrolle über die HTTP-Kommunikation gibt: von einfachen Weiterleitungen über Dateidownloads bis hin zur Absicherung deiner Anwendung mit Security-Headern.
Der Schlüssel zum Erfolg liegt darin, die Basics zu beherrschen, Fehlerquellen zu kennen und defensiv zu programmieren. Arbeite sauber, teste regelmäßig, und baue Sicherheitsmaßnahmen von Anfang an ein.
So entwickelst du Anwendungen, die nicht nur funktionieren, sondern auch sicher, performant und wartbar sind.
Wer tiefer einsteigen will: Beschäftige dich mit Themen wie CORS, Content Security Policy und modernen Security-Headern. Damit setzt du professionelle Standards für deine Webprojekte.