Indice dei contenuti dell'articolo:
Introduzione
Chi lavora quotidianamente con i web server sa che i dettagli fanno la differenza. Una piccola variabile mancante, un comportamento non documentato o una svista nell’implementazione di un protocollo possono rendere inutilizzabili funzioni che, sulla carta, dovrebbero migliorare prestazioni e sicurezza.
Noi di Managed Server Srl ci siamo imbattuti in uno di questi casi concreti, lavorando su un progetto reale, un ecommerce basato su Magento 2. Un problema apparentemente marginale – l’assenza di $http_host
nelle connessioni HTTP/3 – si è rivelato invece un ostacolo reale all’adozione del protocollo, tanto da bloccare l’attivazione nativa di HTTP/3 sul sito.
Il percorso che ci ha portati a individuare, documentare e infine correggere questo bug è stato tortuoso: abbiamo provato prima con il team ufficiale NGINX (senza successo), poi abbiamo deciso di rivolgerci direttamente ad ANGIE, il fork russo sviluppato da ex ingegneri NGINX dopo la crisi con F5 Networks.
Il risultato? Una patch accettata e già integrata nel codice di ANGIE (commit 140c3a6), che risolve una volta per tutte un bug noioso ma molto impattante.
Questa è la cronaca dettagliata di quella vicenda.
Il contesto: HTTP/3 e NGINX
HTTP/3 non è soltanto “l’ultima versione del protocollo HTTP” che segue la numerazione progressiva delle specifiche precedenti. È, a tutti gli effetti, un cambio di paradigma nel modo in cui i browser e i server comunicano su Internet.
Per comprenderne la portata, occorre ricordare come eravamo abituati a lavorare con le versioni precedenti:
- Con HTTP/1.1, le connessioni TCP erano persistenti, ma ogni richiesta portava con sé overhead significativo. Il risultato era spesso un caricamento lento delle pagine ricche di asset (immagini, CSS, JS).
- Con HTTP/2, introdotto ufficialmente nel 2015, si è fatto un enorme passo avanti grazie al multiplexing sullo stesso canale TCP: più richieste potevano convivere senza dover aprire connessioni separate. Tuttavia, TCP rimaneva il tallone d’Achille, soprattutto nei casi di packet loss. Bastava un pacchetto mancante per bloccare l’intero flusso fino alla sua ritrasmissione: il cosiddetto head-of-line blocking.
Con HTTP/3 la prospettiva cambia completamente:
- Si abbandona TCP in favore di QUIC, un protocollo costruito su UDP che integra direttamente funzionalità di ritrasmissione, controllo di congestione e sicurezza crittografica.
- La latenza viene ridotta drasticamente, soprattutto nelle fasi di handshake: con QUIC, la negoziazione TLS è integrata nel protocollo stesso, eliminando round-trip superflui.
- Il problema dell’head-of-line blocking sparisce: ogni flusso viaggia indipendentemente, senza che un pacchetto perso blocchi l’intera connessione.
Non a caso, grandi player come Cloudflare, Google, Meta, Akamai e molti CDN hanno spinto in modo deciso verso l’adozione di HTTP/3. Già oggi una quota consistente del traffico mondiale viaggia su questo protocollo, anche se l’utente finale spesso non se ne rende conto.
Dal punto di vista sistemistico, abilitare HTTP/3 in un web server moderno come NGINX sembrerebbe un’operazione banale: si compila la versione con il supporto a QUIC e HTTP/3 (oggi rilasciato di default in tutti i pacchetti rpm e deb delle ultime versioni NGINX), si aggiungono poche direttive in configurazione e, teoricamente, il gioco è fatto. In realtà, in alcuni rari casi, la differenza tra manuale e produzione è abissale. È lì che entrano in gioco le sottigliezze, le variabili mancanti, le incompatibilità con applicazioni complesse come i CMS o gli ecommerce.
Il caso reale: bselfie.it su Magento 2 — quando $http_host
fa la differenza
All’inizio del 2024 abbiamo ereditato la gestione sistemistica di bselfie.it, un ecommerce di cosmetica tecnica basato su Magento 2. Il passaggio è avvenuto da un’architettura Amazon AWS a una nostra infrastruttura ottimizzata, con un taglio dei costi di circa il 90% e prestazioni sensibilmente superiori.
La riduzione dei costi è stata possibile perché abbiamo eliminato diversi livelli ridondanti (servizi gestiti “as a service”, eccesso di istanze e componenti non essenziali) e consolidato su server dedicato NVMe con storage OpenZFS e stack NGINX + PHP-FPM + Varnish Cache + MariaDB/Redis + ElasticSearch ottimizzato per Magento 2.
Tuttavia non siamo stati in grado di aggiungere la ciliegina sulla torta abilitando HTTP/3. Ogni volta che lo facevamo nel modo “classico”, automaticamente non funzionavano più gli store multilingua in tedesco ed in inglese.
Nel nostro caso specifico, la piattaforma era NGINX 1.28.3, con un’installazione di Magento 2 che ospitava tre siti multilingua: italiano, inglese e tedesco.
Per gestire il multi-store Magento, adottavamo una configurazione (ereditata dalla vecchia azienda che li seguiva) atipica basata sulla direttiva map
, che utilizza il valore di $http_host
per determinare il codice di esecuzione (MAGE_RUN_CODE
) corretto.
In pratica, la logica era questa:
map $http_host $MAGE_RUN_CODE { www.bselfie.it website_it; www.bselfie.it/en website_en; www.bselfie.de website_de; }
Perché questa configurazione funzionava perfettamente con HTTP/2
Con HTTP/2, la variabile $http_host
viene valorizzata correttamente a partire dall’header Host
.
Quindi:
- Una richiesta a
www.bselfie.it
attivava lo store italiano. - Una richiesta a
www.bselfie.it/en
attivava lo store inglese. - Una richiesta a
www.bselfie.de
attivava lo store tedesco.
La mappatura basata su $http_host
era affidabile, coerente e garantiva che ogni lingua/market ricevesse la propria configurazione corretta.
Cosa accade con HTTP/3
Con l’abilitazione di HTTP/3, lo scenario cambia drasticamente.
Questo protocollo non utilizza più direttamente l’header Host
, ma lo pseudoheader :authority
.
Il problema è che, nella nostra configurazione, $http_host
non viene popolato automaticamente da :authority
.
Risultato:
$http_host
rimane vuoto in HTTP/3.- La direttiva
map
non ha più un valore di ingresso valido. - Tutte le richieste finiscono per ricadere nello store di default (in questo caso quello italiano).
Esempio pratico:
- Una richiesta a
www.bselfie.it/en
non attivava piùwebsite_en
, perché$http_host
era vuoto. - Magento interpretava la richiesta come se fosse diretta allo store principale, rompendo di fatto la logica multi-store.
In altre parole: il routing si spegneva e l’intera infrastruttura multilingua collassava su un unico store. Ciò non era accettabile e tra avere un sito con il multilingua rotto ed un sito perfettamente funzionante con HTTP/3 disabilitato, abbiamo optato per questa opzione visto che comunque il TTFB rimaneva comunque buono anche con solo http72.
Perché $host
non era una soluzione praticabile
Una possibile obiezione tecnica potrebbe essere: “Perché non sostituire $http_host
con $host
?”.
È vero: nel mondo NGINX/ANGIE questa è ormai diventata la prassi standard.
Quando si migra una configurazione già esistente da HTTP/2 a HTTP/3, quasi sempre si consiglia di sostituire $http_host
con $host
, perché quest’ultima variabile viene popolata correttamente anche con HTTP/3.
Nella maggior parte dei casi questo “workaround” funziona senza problemi:
$host
contiene il dominio richiesto dal client,- è gestito direttamente dal core di NGINX,
- ed evita di ritrovarsi con una variabile vuota come accade invece con
$http_host
in HTTP/3.
Tuttavia, nel nostro caso specifico — un Magento 2 multilingua e multi-store come bselfie.it — $host non era un sostituto adeguato. Ecco i motivi principali.
1) $host
contiene solo il dominio
$host
valorizza esclusivamente il nome del dominio, ad esempiowww.bselfie.it
.- Non tiene conto di parti aggiuntive come
/en
o/de
, che nella nostra configurazione erano fondamentali per determinare correttamente lo store da servire. - Risultato: tutte le richieste sarebbero ricadute sullo stesso dominio, perdendo la distinzione linguistica e facendo collassare il routing multi-store.
2) Fallback non desiderato
- Se l’header
Host
manca o è invalido,$host
cade sul primoserver_name
del blocco di configurazione o addirittura sull’IP del server. - In un contesto Magento multilingua, questo significa indirizzare richieste legittime allo store sbagliato, con conseguenze imprevedibili sui flussi utente e sulle conversioni.
3) Incoerenza con la configurazione esistente
- Tutto lo stack Magento (e molti moduli di terze parti) si aspettano che la variabile
HTTP_HOST
corrisponda esattamente all’host fornito dal client. - Alterare questo comportamento forzando
$host
al posto di$http_host
porta a redirect non voluti, loop infiniti o URL generati in modo incoerente.
Per la maggior parte dei siti e delle applicazioni web, insomma, sostituire $http_host
con $host
è un trucco rapido, usato praticamente sempre per far funzionare HTTP/3 con configurazioni esistenti di NGINX.
Ma nel caso di bselfie.it e di altri scenari complessi con routing multi-store o multi-lingua, $host
è semplicemente troppo “normalizzato” e povero di informazioni per reggere il carico di logica applicativa che Magento richiede.
Di conseguenza, era indispensabile che $http_host
venisse popolato correttamente anche in HTTP/3, attingendo allo pseudoheader :authority
.
La proposta di fix
Dall’analisi che abbiamo fatto sul caso reale di bselfie.it, la conclusione è stata immediata:
NGINX (e i fork come ANGIE) dovrebbero popolare $http_host
anche in HTTP/3, prelevandolo direttamente dal valore dello pseudohader :authority
.
Non stiamo parlando di un cambiamento rivoluzionario, ma semplicemente di mantenere la consistenza cross-protocollo:
- in HTTP/1.1 e HTTP/2,
$http_host
è correttamente popolato dall’headerHost
; - in HTTP/3, dove
Host
non esiste più e viene sostituito da:authority
, è naturale che$http_host
debba ereditare questo valore.
In questo modo:
- le applicazioni che dipendono da
$http_host
(come Magento 2 nel nostro caso) continuerebbero a funzionare senza alcuna modifica; - non sarebbe necessario intervenire manualmente sulla configurazione con workaround come:
fastcgi_param HTTP_HOST $host;
che, come abbiamo spiegato, funziona nella maggior parte degli scenari ma non in setup avanzati e multistore.
A noi è sembrata la cosa più logica e pacifica: non rompere ciò che già funziona in HTTP/2, semplicemente garantendo che in HTTP/3 $http_host
venga riempito da :authority
.
Cronologia essenziale delle richieste
- 16 gennaio 2025 — Apriamo la segnalazione su NGINX spiegando che in HTTP/3
$http_host
resta vuota perché non viene inizializzata da:authority
, proponendo di allineare il comportamento a HTTP/1.1 e HTTP/2.
Issue: “Support for populating $http_host with the :authority header in HTTP/3 (QUIC)” (NGINX #455). - 23 gennaio 2025 — Riproponiamo la stessa richiesta su ANGIE (fork drop-in di NGINX).
Issue: “Support for populating $http_host with the :authority header in HTTP/3 (QUIC)” (ANGIE #109). - 12 agosto 2025 — ANGIE accetta e integra la patch: “HTTP/3: initialize ‘Host’ header from authority.” Il commit (SHA
140c3a6
) inizializza correttamenteHost
/$http_host
partendo da:authority
quando il campoHost
non è presente, allineandosi alla RFC 9114. - 29 Settembre 2025 — Valentin V. Bartenev “VBart” il manutentore di ANGIE della Web Server LLC di Mosca, ci comunica che la nostra richiesta è stata chiusa comunicandoci l’implementazione e la successiva distribuzione nella prossima release di ANGIE.
È curioso che, anche in ANGIE, la prima risposta del manutentore — arrivata il giorno successivo all’apertura della nostra richiesta — sia stata di usare $host
al posto di $http_host
, cassando di fatto la proposta. Un suggerimento che, pur essendo prassi diffusa nelle migrazioni a HTTP/3, nel nostro contesto non avrebbe mai retto in produzione né sul piano funzionale né su quello della coerenza con le configurazioni esistenti. Poi, con un’analisi più attenta condotta ad agosto, la posizione è cambiata: si è riconosciuto che inizializzare $http_host
da :authority
è la scelta corretta e, più che un’eccezione, rappresenta la norma per garantire continuità tra HTTP/2 e HTTP/3 senza rompere gli stack applicativi.
Dal punto di vista delle specifiche, la RFC 9114 (HTTP/3) sezione 4.3.1 “Request Pseudo-Header Fields” chiarisce che i client devono usare :authority
al posto di Host
e, quando entrambi sono presenti, devono contenere lo stesso valore. L’implementazione in ANGIE rende quindi il comportamento di HTTP/3 coerente con quello di HTTP/2 anche sul piano pratico delle variabili server.
di cui relativa traduzione in lingua italiana :
Se il campo pseudo-header
:scheme
identifica uno schema che prevede obbligatoriamente una componente di authority (inclusi"http"
e"https"
), la richiesta DEVE contenere o il campo pseudo-header:authority
oppure l’headerHost
. Se questi campi sono presenti, NON DEVONO essere vuoti. Se entrambi i campi sono presenti, DEVONO contenere lo stesso valore. Se lo schema non prevede una componente di authority obbligatoria e nessuna viene fornita nel target della richiesta, la richiesta NON DEVE contenere i campi pseudo-header:authority
o l’headerHost
.Una richiesta HTTP che omette campi pseudo-header obbligatori o contiene valori non validi per tali campi pseudo-header è malformata.
HTTP/3 non definisce un modo per trasportare l’identificatore di versione incluso nella request line di HTTP/1.1. Le richieste HTTP/3 hanno implicitamente una versione di protocollo pari a “3.0”.
Perché per noi questa soluzione era pacifica e “logicamente perfetta”
Vale la pena chiarire perché, dal nostro punto di vista operativo e architetturale, questa scelta fosse ovvia. L’obiettivo era preservare il comportamento atteso dalle applicazioni al passaggio da HTTP/2 a HTTP/3, evitando workaround fragili e garantendo continuità senza toccare configurazioni collaudate. In altre parole: massima compatibilità, minima frizione, pieno allineamento agli standard.
- Coerenza cross-protocollo: in HTTP/1.1 e HTTP/2
$http_host
è sempre valorizzata; in HTTP/3 l’equivalente semantico è:authority
. Popolare$http_host
da:authority
evita regressioni su stack applicativi che si aspettanoHTTP_HOST
(Magento multi-store in primis). - Zero interventi sulle configurazioni esistenti: non serve introdurre o diffondere il workaround con
$host
, che in casi complessi può portare a inconsistenze. - Aderenza esplicita alle specifiche: l’aggancio a
:authority
è proprio ciò che la RFC indica come sorgente autorevole lato client; l’implementazione server-side che ne deriva è naturale.
Oltretutto non significava fare un’impresa titanica, ma semplicemente popolare una variabile vuota con il contenuto di un’altra variabile se presente. Nulla di più nulla di meno.
Perché prima NGINX e poi ANGIE
La nostra strategia è stata chiara fin dall’inizio: ottenere la validazione e l’introduzione della feature da almeno uno dei due progetti, NGINX o ANGIE, così da creare un precedente tecnico forte che, nel giro di pochi giorni o al più qualche mese, spingesse anche la controparte ad allinearsi per semplice inerzia tecnica e coerenza con le specifiche (RFC 9114).
Questa linea d’azione si inserisce dentro una cornice storica e geopolitica non banale: NGINX nasce in Russia nei primi anni 2000 (guidato da Igor Sysoev), cresce fino a diventare un pilastro del web moderno e viene poi acquisito da F5 Networks; con lo scoppio della guerra tra Russia e Ucraina, il quadro si complica, F5 chiude la filiale russa e diversi ingegneri storici – rimasti senza sede – fondano ANGIE in Russia, un drop-in replacement compatibile con la sintassi e la filosofia di NGINX, tecnicamente molto reattivo. In questo contesto, da azienda europea, siamo tenuti a una compliance rigorosa: tra sanzioni, vincoli regolatori, gestione del rischio Paese e policy interne, non possiamo adottare ANGIE in produzione, per quanto ne riconosciamo il valore tecnico.
Ecco perché abbiamo proceduto così: prima NGINX, che è il web server che usiamo tutti i giorni e il naturale destinatario della nostra proposta; poi ANGIE, per aumentare le probabilità che la soluzione venisse analizzata e messa a terra rapidamente, creando quel “effetto precedenza” capace di trascinare anche l’altro progetto. In sostanza, volevamo che uno dei due implementasse per primo la mappatura di :authority
su $http_host
in HTTP/3, evitando regressioni applicative e workaround fragili: se l’avesse fatto ANGIE (come poi è accaduto), NGINX avrebbe avuto un ottimo incentivo a convergere; se l’avesse fatto NGINX, ANGIE avrebbe seguito per compatibilità e per la sua stessa ambizione di essere un sostituto drop-in. Una strategia semplice, concreta e coerente con la realtà: mettere in produzione la soluzione dove c’è più velocità di integrazione, e lasciare che il resto dell’ecosistema si riallinei di conseguenza.
Ricordiamo che ANGIE è un “drop-in replacement” di NGINX: in pratica significa che può sostituire NGINX senza (o con minime) modifiche a configurazione, comandi e flussi operativi. La sintassi di nginx.conf
, le direttive (server, location, map, upstream, ecc.), la disposizione dei virtual host, i flag da riga di comando e il comportamento dei principali moduli (http/stream/mail) sono mantenuti compatibili, così da poter rimpiazzare il binario e continuare a usare gli stessi file e le stesse procedure.
Il codice di ANGIE è in larga parte derivato/compatibile con quello di NGINX, per cui la patch che inizializza $http_host
da :authority
è concettualmente portabile anche su NGINX con un diff minimo (fermo restando differenze di tree, build system, moduli dinamici e politiche di licenza). In altre parole, la correzione introdotta in ANGIE è tecnicamente backportabile su NGINX, ed è proprio questo il valore di un vero drop-in: compatibilità operativa immediata e facilità di re-uso del lavoro tra i due progetti.
Conclusioni e considerazioni
In chiusura, quello che continua a lasciarci perplessi è come una richiesta semplice, ben documentata e perfettamente allineata alla RFC sia stata accolta con scarso interesse da NGINX (F5 Networks), mentre — pur dopo un iniziale invito a “ripiegare su $host
” — sia stata invece riconosciuta valida e implementata dagli ex sviluppatori russi di NGINX nel fork ANGIE. Dal nostro punto di vista operativo, la proposta era di puro buon senso: popolare $http_host
da :authority
in HTTP/3 per garantire continuità con HTTP/1.1/HTTP/2 ed evitare regressioni sulle applicazioni in produzione. È un miglioramento piccolo ma ad alto impatto, soprattutto in ecosistemi complessi come Magento multi-store.
C’è poi un piano più ampio, quasi “culturale”. È difficile non notare che l’anima originaria di NGINX — quella fatta di pragmatismo e attenzione alla produzione reale — sembri essere rimasta in Russia, nell’ormai chiusa filiale da cui è nato il gruppo che ha dato vita ad ANGIE. Limitazioni geopolitiche permettendo, in un auspicabile futuro di pace saremmo ben lieti di spostare i nostri carichi verso ANGIE: i fatti dimostrano che il team ha saputo ascoltare, valutare e mettere a terra una correzione mirata e conforme agli standard, confermando — con un pizzico di ironia — che “i russi lo fanno meglio”. Non si tratta di tifo: è valutazione tecnica della responsività e della qualità dell’implementazione.
Sul fronte NGINX, da parte nostra c’è stato un ulteriore passo di responsabilità verso l’ecosistema: abbiamo aggiornato la proposta originaria aperta a gennaio 2024, evidenziando che ANGIE ha già integrato il fix e richiamando esplicitamente la RFC 9114. Il messaggio è chiaro: non chiediamo una rivoluzione, ma consistenza cross-protocollo e tutela dei deployment reali. Ci auguriamo che questa volta la richiesta non venga presa sottogamba e che la vedremo in produzione nella prossima 1.29.2 entro fine ottobre 2025, o al più entro la fine del 2025. Sarebbe un segnale importante: per chi sviluppa in upstream, per chi mantiene in produzione e, soprattutto, per chi — come noi — lavora ogni giorno perché il web sia più veloce, affidabile e coerente dagli standard fino al codice che serve le pagine agli utenti, a volte anche – come in questo caso – sporcandosi le mani – a differenza di nostri “colleghi” venditori di hosting della domenica – che si limitano a scrivere post su come “Installare un plugin WordPress”, o su come creare una casella mail su Plesk o cPanel.
Patetici.