
Lieber Administrator, der du „schon seit Jahren“ nginx verwendest und immer wieder davon schwärmtest, wie toll, schnell und einfach zu konfigurieren er sei: Es ist sehr gut möglich, dass du in deinen Konfigurationsdateien einige Zeitbomben versteckt hast. Sie heißen $http_host und du solltest sie dringend durch $host ersetzen. Offenbar sind viele Dokumentationen im Internet einfach falsch. Danke mir später für diesen Tipp.
Es gibt einige Anwendungsfälle, in denen man in der Konfiguration gerne wissen möchte, über welche Domain wir gerade sprechen. Der älteste Trick im SEO-Buch ist eine Zwangsweiterleitung auf die „richtige“ Domain, also mit oder ohne www. Normalerweise lässt man den Server auf beides hören, da viele Nutzer instinktiv Domains mit einem www. in die Adresszeile eingeben. In dem Fall ist die Variable $host in nginx die einzig richtige. Sie war es schon immer, und wenn du auf HTTP/3 umstellst, wirst du am eigenen Leib erfahren, warum das so ist.
Das Problem mit HTTP/3
Wenn du HTTP/3 mit seinem flinken, UDP-basierten QUIC-Protokoll nutzen möchtest, führt im Prinzip kein Weg an nginx vorbei. Denn Old Trusty (Apache) kann es nicht und wird es vermutlich auch so schnell nicht lernen, da es nur TCP spricht. (Wenn dir LLMs wie ChatGPT oder Gemini sagen, dass es „nicht-öffentliche“ Module geben soll, lügen sie; das haben sie von einer Quelle, die komplett ohne Belege auskommt und auf die sich mehrere andere Berichte beziehen.)
Aber hier ist das Problem: Variablen mit dem Präfix $http_ sind in nginx sämtliche HTTP-Header-Variablen, die der Client übermittelt. Das erste Problem ist also, dass wir einem Client nicht vertrauen sollten, da spätestens Bots gerne mal übermitteln, was sie wollen – vielleicht in der Hoffnung, eine coole Sicherheitslücke zu finden (oder auszunutzen). Zudem kann $http_host noch allerlei Unfug enthalten, etwa Port-Angaben oder mit Groß- und Kleinschreibung hantieren.
$host: Die zuverlässige Alternative
Auf der anderen Seite steht die $host-Variable. Sie kommt von nginx und wird von der Software selbst gesetzt. Sie enthält die normalisierte Domain, um die es gerade geht, nachdem der Server anhand der Indizien vom Client entschieden hat, was passieren soll.
Der Inhalt kann dabei aus dem Host:-Header kommen, aus der :authority, aus der SNI für SSL oder es kann auch der erste server_name im server {}-Block sein. Auf jeden Fall kannst du dich darauf verlassen – nicht nur, dass $host gesetzt ist, sondern auch, dass es die Information enthält, die du suchst: die aktuelle Domain, in Kleinbuchstaben, und nichts anderes.
Das größte Problem ist aber, dass der Host:-Header in HTTP/3 (und schon in HTTP/2) gar nicht mehr benötigt wird. Stattdessen arbeiten die Systeme mit dem :authority-Pseudo-Header. Der Host:-Header kann zwar gesetzt sein, etwa bei der ersten Verbindung, wenn der Browser aus Gründen der Kompatibilität noch nicht erwartet, dass HTTP/2 oder HTTP/3 zur Verfügung steht, aber er muss es nicht.
„Aber es hat doch immer funktioniert?“
nginx war im HTTP/2-Modul „nett“. Es hat $http_host immer gesetzt – wenn es eigentlich leer gewesen wäre, hat es den Inhalt von $host kopiert. Diese Nettigkeit existiert im HTTP/3-Modul aber nicht mehr, und wie die Entwickler auf GitHub schreiben, ist dies absichtlich der Fall und wird demzufolge voraussichtlich nicht geändert.
Mit anderen Worten: Wenn du deine aktuelle Domain suchst, dann ist $host die richtige Antwort und NICHT $http_host! Das war im Prinzip schon immer so, aber mit HTTP/3 wird es dir um die Ohren fliegen, wenn du dich weiterhin auf $http_host verlässt.
Ein weitverbreitetes Missverständnis
Ich habe, für HTTP/3, den Wechsel von Apache auf nginx vollzogen. Und während ich den Indianer gut kenne und, wie ich meine, auch einigermaßen verstehe, fühlt sich nginx für mich an wie ein fremdes Land, dessen Sprache ich nicht spreche und dessen Kultur ich nicht kenne. Also bat ich Gemini um Hilfe bei der Umwandlung von .htaccess auf die nginx-Konfiguration. Die Regel mit der Zwangsweiterleitung sorgte dann für eine Endlosweiterleitung, weil genau das passiert ist, wovor ich hier warne: $http_host ist leer, also schlägt die Abfrage if ($http_host != "bsod.wtf") fehl und er versucht, auf bsod.wtf weiterzuleiten – wo genau dasselbe nochmal passiert.
Dass mir Gemini die falsche Information gab, ist ein Indiz dafür, dass es ein weitverbreitetes Missverständnis sein muss. Denn LLMs werden mit Informationen gefüttert und sind dann ein Wahrscheinlichkeitsrechner. Kommt eine Information oft vor, ist sie vermutlich „richtiger“ als eine Information, die im selben Kontext seltener vorkommt.
Ein weiteres Beispiel ist der nginx-Companion für WP Rocket (nicht von den WP-Rocket-Entwicklern). Auch er versucht mit der $http_host-Variable die Struktur auf dem Dateisystem nachzubilden und zu schauen, ob es eine gecachte Version des aktuellen Requests gibt. Wenn $http_host jedoch leer ist, wird die Prüfung fehlschlagen und wir bekommen einen MISS. WP Rocket fällt dann auf die PHP-Version via advanced_cache.php zurück, insofern geht nichts kaputt und man merkt es vielleicht nicht einmal. Aber es belastet dennoch einen PHP-Worker, und Sinn der Übung ist ja eigentlich eine Auslieferung so schnell wie möglich. Gegen das pure Ausliefern einer statischen HTML-Datei kann PHP einfach nicht anstinken.
PHP-FPM Konfiguration: $_SERVER['HTTP_HOST'] korrigieren
Aber es gibt noch einen Elefanten im Raum, und der heißt PHP (see what I did there?). PHP wird mit nginx über FPM geladen, und damit übergibt man der Sprache einige wichtige Informationen. Das ist beispielsweise der Query-String, der Script-Name, die Remote-Addr (IP des Client) und vieles andere mehr. Zudem bekommt PHP sämtliche HTTP-Header.
Das führt zu der unschönen Situation, dass auch $_SERVER['HTTP_HOST'] leer sein kann, und das ist fatal, da sich sehr viele Scripte auf dessen Existenz verlassen. Also solltest du in der /etc/nginx/fastcgi_params noch diese Zeile ergänzen (falls sie nicht bereits existiert):
fastcgi_param HTTP_HOST $host;
In der GitHub-Issue schiebt nginx seine Zuständigkeit dafür zum fastcgi-Modul:
Probably it’s fastcgi module responsibility to care about passing the correct authority in the HTTP_HOST parameter
Fazit: Check deine Konfiguration!
Kurz gesagt: $http_host ist so gut wie immer die falsche Antwort, und du solltest deine Konfigurationsdateien überprüfen. Wenn du auf $http_host stößt, dann ändere es in $host. Spätestens, wenn das Upgrade auf HTTP/3 ansteht, wirst du mir danken.
Und für mich: Dem goldenen Drachen sei Dank, dass ich explizit wegen HTTP/3 zu nginx gewechselt bin. Da bin ich wohl einer gigantischen Kugel ausgewichen. Einer Zeitbombe, die auf ihren großen Auftritt wartet. So konnte ich sie im Chaos der eigentlichen Migration schon entschärfen.