SSD Nodes Learn 🎉 VPS da $5.50/mese
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-08-13

HTTP: guida per amministratori di server

Scopri HTTP dal lato server: metodi, codici di stato, header essenziali, log di nginx e differenze pratiche tra HTTP/3 e TLS.

Che cos’è HTTP?

HTTP (hypertext transfer protocol) è l’insieme di regole che un client e un web server usano per richiedere una risorsa e restituirla. Il client invia una richiesta composta da un metodo come GET, un percorso come /pricing, una versione del protocollo, un elenco di header e, talvolta, un body. Il server risponde con un codice di stato come 200, seguito dai propri header e, di norma, da un body. Ogni visualizzazione di una pagina e ogni chiamata API (application programming interface) al server corrispondono a questo scambio, che si ripete.

HTTP non conserva autonomamente lo stato. Il server non ricorda ciò che è stato richiesto un secondo prima. Per questo, qualsiasi informazione che richieda memoria, ad esempio una sessione di accesso, viene trasmessa in un header a ogni singola richiesta. Questa proprietà spiega gran parte di ciò che segue: la cache è gestita interamente tramite header e un load balancer può inoltrare la richiesta successiva a un backend diverso senza causare problemi.

Tutto ciò che segue mostra come questo modello si presenta dal lato server, nel log degli accessi e nella configurazione di nginx.

Una richiesta e una risposta grezze, con annotazioni

Questa è una richiesta HTTP/1.1 completa. Una riga vuota termina gli header; tutto ciò che segue quella riga costituisce il body. Una GET normalmente non ha un body.

GET /pricing HTTP/1.1
Host: example.com
User-Agent: curl/8.5.0
Accept: */*
Accept-Encoding: gzip
  • GET è il metodo, che indica l'operazione richiesta. GET legge, POST invia dati, PUT sostituisce, DELETE rimuove, HEAD richiede gli header di una GET senza il body.
  • /pricing è il percorso. Il nome host non fa parte della request line; per questo esiste l'header successivo.
  • HTTP/1.1 è la versione del protocollo utilizzata dal client.
  • Host: example.com indica il sito richiesto dal client. HTTP/1.1 lo richiede, quindi nginx risponde a una richiesta priva di questo header con 400 Bad Request.
  • Gli altri header esprimono preferenze. Accept-Encoding: gzip indica che il client può decomprimere i dati, quindi il server può comprimere il body.

La risposta ha la stessa struttura, con una status line all'inizio.

HTTP/1.1 200 OK
Date: Thu, 06 Aug 2026 09:12:44 GMT
Server: nginx
Content-Type: text/html; charset=utf-8
Content-Length: 5310
Cache-Control: public, max-age=300

<!doctype html>...
  • 200 OK è il codice di stato con la relativa reason phrase. Il codice è l'informazione importante. La frase è solo descrittiva e i client la ignorano.
  • Content-Type indica al client come interpretare i byte successivi.
  • Content-Length è la dimensione del body in byte, quindi il client sa dove termina il body. Quando la dimensione non è nota in anticipo, il server invia invece Transfer-Encoding: chunked e indica la fine con un chunk di lunghezza zero.
  • Cache-Control indica al browser e agli eventuali cache intermedi per quanto tempo possono conservare questa risposta.
  • La riga vuota dopo gli header li separa dal body, in entrambe le direzioni.

I nomi degli header non distinguono tra maiuscole e minuscole e ogni riga termina con un carriage return seguito da un line feed, non con un semplice newline. Non li inserirai manualmente, ma li incontrerai in una cattura dei pacchetti.

Per osservare una coppia reale, esegui questo comando verso un sito che amministri:

curl -sS -o /dev/null -D - https://example.com/

-D - scrive gli header della risposta nel terminale e -o /dev/null scarta il body. Preferisci questa opzione a curl -I, perché -I invia una richiesta HEAD. Un application server che gestisce HEAD in modo diverso da GET, come accade spesso, mostrerà quindi header che nessun browser riceve. curl -v stampa entrambe le direzioni, contrassegnando le request line con > e le response line con <.

Come appare la riga di richiesta nel log degli accessi di nginx

nginx include un formato di log combined, definito come segue:

log_format combined '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    '"$http_referer" "$http_user_agent"';

Ecco una riga generata da questo formato:

203.0.113.45 - - [06/Aug/2026:09:12:44 +0000] "GET /pricing HTTP/1.1" 200 5310 "https://example.com/" "Mozilla/5.0 (X11; Linux x86_64) Chrome/127.0.0.0 Safari/537.36"
  • 203.0.113.45 è $remote_addr, l'indirizzo che ha aperto la connessione TCP (Transmission Control Protocol). Se è presente un proxy, questo è l'indirizzo del proxy, non quello del client.
  • Il primo - è un segnaposto fisso. Il secondo è $remote_user e viene valorizzato solo quando si usa l'autenticazione HTTP Basic.
  • "GET /pricing HTTP/1.1" è $request, la riga di richiesta copiata esattamente nel formato in cui è arrivata.
  • 200 è lo stato restituito dal server, non quello osservato dal client.
  • 5310 è $body_bytes_sent, cioè il solo corpo della risposta. Gli header della risposta non vengono conteggiati, quindi questo numero è sempre inferiore ai byte effettivamente inviati.
  • Gli ultimi due campi tra virgolette sono Referer e User-Agent. Entrambi provengono dal client, quindi possono contenere qualsiasi valore.

Poiché $request viene copiato senza modifiche, anche i dati anomali compaiono senza modifiche. Un client che usa TLS (Transport Layer Security) sulla porta HTTP in chiaro 80 lascia una riga 400 il cui campo della richiesta inizia con byte con escape come "\x16\x03\x01\x02\x00\x01". \x16 è il tipo di record dell'handshake TLS, quindi quei byte sono l'inizio di un ClientHello e non costituiscono affatto una riga di richiesta. Il server si comporta correttamente. Qualcosa sta inviando richieste HTTPS a una porta HTTP.

Aggiungi anche $server_protocol al formato di log. Stampa HTTP/1.1, HTTP/2.0 o HTTP/3.0 ed è il modo più rapido per verificare che una modifica del protocollo sia stata effettivamente applicata.

Che cosa significano i codici di stato comuni quando è il tuo sito a restituirli

La prima cifra indica la classe, ed è la classe che devi leggere per prima.

2xx significa che la richiesta è riuscita. 200 OK per una lettura normale. 201 Created dopo un POST che ha creato una risorsa. 204 No Content per una richiesta riuscita senza nulla da restituire, che è la risposta usuale a un DELETE.

3xx significa che devi cercare altrove. 301 indica uno spostamento permanente e i browser lo memorizzano a lungo, talvolta finché l'utente non cancella il proprio profilo; per questo un 301 che punta al nome host sbagliato è difficile da correggere. Usa 302 mentre stai ancora testando un redirect. 304 Not Modified indica un esito positivo, non un errore: il client ha inviato If-None-Match con un ETag (entity tag) che riconosci ancora, quindi hai risposto con gli header e senza body. Un log pieno di 304s indica che il caching funziona.

4xx significa che la richiesta era errata. 400 Bad Request indica un input malformato. 401 Unauthorized significa realmente che il client non è autenticato e deve includere un header WWW-Authenticate che specifichi lo schema. 403 Forbidden significa che la richiesta è stata compresa, ma è stata comunque rifiutata. 404 Not Found indica un percorso inesistente. 405 Method Not Allowed indica il percorso corretto con il metodo errato, come nel caso di un POST verso una posizione con file statici. 413 indica un body più grande di client_max_body_size di nginx, che per impostazione predefinita è di 1 megabyte; il log degli errori lo conferma con client intended to send too large body.

Un 403 su un file statico dipende quasi sempre dal filesystem, non da una regola HTTP. Leggi /var/log/nginx/error.log prima di modificare la configurazione. open() "/srv/site/index.html" failed (13: Permission denied) significa che l'utente worker di nginx non può leggere il file, nella maggior parte dei casi perché una directory padre non concede agli altri il permesso di esecuzione. directory index of "/srv/site/" is forbidden significa che il percorso è stato risolto in una directory senza un file index mentre autoindex è disattivato.

5xx significa che il problema è sul tuo lato. 500 indica un errore non gestito nell'applicazione. 502 Bad Gateway significa che nginx non ha potuto ottenere una risposta utilizzabile dall'upstream, e il log degli errori ne indica la causa: connect() failed (111: Connection refused) while connecting to upstream significa che non c'è alcun processo in ascolto sull'indirizzo indicato da proxy_pass. 504 Gateway Timeout significa che l'upstream ha accettato la connessione, ma non ha inviato dati entro proxy_read_timeout, 60 secondi per impostazione predefinita; il log lo registra come upstream timed out (110: Connection timed out) while reading response header from upstream. 503 Service Unavailable indica un rifiuto intenzionale. Tieni presente che il rate limiter integrato di nginx restituisce 503, perché limit_req_status è impostato per impostazione predefinita su 503. Se stai cercando 429 Too Many Requests nel log e trovi invece 503, il motivo è questo. Imposta limit_req_status 429; per ottenere il codice corretto.

Le intestazioni importanti quando si esegue il server

Host seleziona il sito. Un unico indirizzo IP può servire centinaia di nomi host e nginx confronta Host con server_name per decidere quale blocco server deve rispondere. Se non trova corrispondenze, nginx usa il server predefinito, cioè il primo blocco in ascolto su quell'indirizzo e quella porta, a meno che un altro blocco non sia contrassegnato con default_server. Se un nuovo virtual host restituisce il sito errato, quasi sempre la causa è questa: il nome non corrispondeva e la richiesta è ricaduta sul server predefinito. È possibile eseguire il test senza modificare il DNS:

curl -sS -o /dev/null -D - -H 'Host: app.example.com' http://127.0.0.1/

User-Agent è una descrizione del client e contiene testo libero. Usatelo come indicazione quando analizzate i log. Non usatelo come criterio di controllo, perché un client che vuole falsificarlo può farlo senza difficoltà. Di conseguenza, bloccare uno scraper in base a User-Agent filtra soltanto i client più corretti.

Content-Type determina come interpretare i byte: application/json per una richiesta API, text/html; charset=utf-8 per una pagina. nginx associa le estensioni dei file ai relativi tipi tramite /etc/nginx/mime.types. Il pacchetto nginx.conf imposta default_type application/octet-stream;, quindi un file con un'estensione che nginx non riconosce viene offerto come download invece di essere visualizzato. Il sintomo visibile è una pagina che si carica senza stile mentre la console del browser mostra Refused to apply style from ... because its MIME type ('text/plain') is not a supported stylesheet MIME type. MIME significa multipurpose internet mail extensions, il sistema di denominazione da cui provengono queste stringhe di tipo.

Cache-Control consente di controllare tutte le cache tra il server e il lettore. public, max-age=31536000, immutable è adatto alle risorse il cui nome contiene un hash del contenuto, perché il nome cambia quando cambia il contenuto. no-store deve essere usato per tutto ciò che è specifico dell'utente, perché una cache condivisa che conserva una pagina dopo il login potrebbe consegnarla alla persona successiva che richiede lo stesso URL. private è l'impostazione intermedia: il browser può conservarla, mentre una cache condivisa non può farlo.

X-Forwarded-For è necessario perché un proxy nasconde l'indirizzo del visitatore. Dopo il passaggio della richiesta attraverso un reverse proxy, $remote_addr è l'indirizzo del proxy. Di conseguenza, i log, la geolocalizzazione e il rate limiting vedono un unico client. Il proxy deve inoltrare l'indirizzo originale:

proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

Il server che riceve la richiesta deve quindi essere configurato per considerarlo attendibile e deve sapere esattamente di quale proxy fidarsi:

set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;

Elencate soltanto le reti che controllate. X-Forwarded-For è testo normale che qualunque client può inviare. Di conseguenza, set_real_ip_from 0.0.0.0/0; consente a un visitatore di scegliere l'indirizzo registrato nei log e quello conteggiato dal rate limiter.

X-Forwarded-Proto previene un errore specifico e molto comune. Il proxy termina TLS e inoltra la richiesta all'applicazione tramite HTTP non cifrato. L'applicazione vede una richiesta HTTP, decide che il visitatore deve usare HTTPS e risponde con 301 https://example.com/. Il browser segue il redirect, il proxy termina nuovamente TLS e inoltra ancora una richiesta HTTP non cifrata. Il ciclo si ripete finché il browser interrompe la richiesta con ERR_TOO_MANY_REDIRECTS. L'invio di X-Forwarded-Proto: https comunica all'applicazione che il visitatore sta già usando HTTPS, quindi l'applicazione interrompe il redirect.

HTTP/1.1 vs HTTP/2 vs HTTP/3: cosa cambia per te

HTTP/1.1 è basato sul testo e gestisce una richiesta alla volta per connessione. Connection: keep-alive consente alla richiesta successiva di riutilizzare la stessa connessione TCP, riducendo il costo di instaurazione, ma le risposte arrivano comunque nell'ordine in cui sono state richieste. Una risposta lenta blocca tutto ciò che è accodato dopo di essa. Questo fenomeno si chiama head-of-line blocking e i browser lo aggirano aprendo contemporaneamente più connessioni allo stesso hostname.

HTTP/2 mantiene gli stessi metodi e gli stessi codici di stato, ma modifica il framing trasformandolo in binario. Più richieste condividono una connessione come stream indipendenti e le porzioni di header ripetute vengono compresse, un aspetto importante perché una richiesta moderna ne contiene molte. La connessione usa ancora TCP, quindi la perdita di un pacchetto blocca tutti gli stream su quella connessione finché non arriva la ritrasmissione. L'head-of-line blocking non è scomparso. Si è spostato da HTTP al livello di trasporto. Il server push faceva parte di HTTP/2, ma in pratica è scomparso perché Chrome ne ha rimosso il supporto nel 2022.

HTTP/3 mantiene ancora una volta la stessa semantica e sostituisce TCP con QUIC, un protocollo di trasporto basato su UDP (user datagram protocol). Gli stream QUIC sono indipendenti a tutti i livelli, quindi un pacchetto perso blocca soltanto lo stream a cui apparteneva. TLS 1.3 è integrato nell'handshake QUIC invece di essere sovrapposto, quindi una nuova connessione richiede meno round trip. Ne derivano due conseguenze pratiche: la porta UDP 443 deve essere aperta in ogni firewall lungo il percorso e qualsiasi rete che limiti o blocchi UDP farà ricadere i client su HTTP/2.

Cosa cambia concretamente per te. I browser non iniziano mai con HTTP/3. Si connettono tramite HTTP/2 o HTTP/1.1, ricevono un header Alt-Svc: h3=":443"; ma=86400 nella risposta e usano HTTP/3 per le connessioni successive verso quell'host. L'header non è quindi un elemento decorativo facoltativo. È il meccanismo di rilevamento. In nginx, HTTP/2 è diventato una direttiva distinta nella versione 1.25.1 (http2 on; all'interno del blocco server, in sostituzione del vecchio parametro listen ... http2), mentre QUIC è arrivato nella versione mainline 1.25.0, dove un sito HTTP/3 richiede listen 443 quic reuseport; insieme al normale listen 443 ssl;.

I proxy differiscono per livello di supporto e maturità, quindi è importante verificare la versione effettivamente in uso. Ad agosto 2026, Caddy serve HTTP/3 per impostazione predefinita senza configurazione aggiuntiva. nginx richiede il listener quic esplicito e l'header Alt-Svc descritto sopra. Traefik lo abilita per ogni entry point tramite un'opzione http3 esplicita. Se termini TLS con Traefik davanti a più applicazioni Docker, la versione del protocollo ricevuta dai visitatori viene decisa in quel punto e il collegamento dal proxy al container è generalmente HTTP/1.1 semplice, indipendentemente dal protocollo negoziato dal browser.

Verifica invece di dare per scontato. curl --http3 -sS -o /dev/null -D - https://example.com/ funziona solo se curl -V elenca HTTP3 tra le proprie funzionalità e la maggior parte delle build delle distribuzioni non lo include. Il controllo più affidabile è il tuo log: aggiungi $server_protocol al formato e verifica quale protocollo negoziano i browser reali. Prima di tutto, conferma che la porta UDP 443 sia effettivamente aperta, perché un firewall che consente solo TCP 443 farà fallire HTTP/3 senza messaggi evidenti, mentre il sito continuerà a funzionare tramite HTTP/2. Sapere quali porte sono aperte e in ascolto sul tuo server Linux è il primo controllo da eseguire.

HTTPS: HTTP è il protocollo, TLS è il livello di protezione

HTTPS non è un protocollo separato. Sono le stesse richieste e gli stessi codici di stato trasmessi all'interno di una sessione TLS. La porta 80 li trasporta in chiaro, mentre la porta 443 li trasporta crittografati. Prima viene completato l'handshake TLS, poi la richiesta HTTP viaggia all'interno del canale crittografato. Per questo un problema del certificato non ha mai un codice di stato associato: l'errore si verifica prima dell'invio di un singolo byte HTTP, quindi non esiste alcuna risposta a cui assegnare un numero.

Su un server che ospita più siti è importante l'ordine di queste operazioni. Il certificato viene scelto usando SNI (server name indication), un campo dell'handshake TLS che contiene il nome host in chiaro, prima che esista un header HTTP. Il server sceglie quindi prima un certificato in base a SNI e poi un virtual host in base all'header Host. Si tratta di due ricerche separate che normalmente restituiscono lo stesso risultato. Quando non coincidono, il browser mostra un errore di mancata corrispondenza del nome, ad esempio NET::ERR_CERT_COMMON_NAME_INVALID, e non invia alcuna richiesta, perché il certificato del server predefinito è stato offerto per un nome che non copre.

Per un sito pubblico, procurati un certificato valido e configura il rinnovo automatico. Certbot con Let's Encrypt su nginx inserisce i percorsi dei certificati nel blocco del server e installa il timer per il rinnovo. Per un nome host che nessuna autorità pubblica può convalidare, ad esempio un nome interno o un indirizzo IP non associato a un nome nella tua rete, un certificato autofirmato su Ubuntu è l'opzione corretta, purché tu accetti che ogni client debba essere configurato per considerarlo attendibile.

Dopo aver verificato che TLS funziona, inoltra tutto il traffico dalla porta 80 alla porta 443:

server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

Aggiungi Strict-Transport-Security solo quando hai la certezza necessaria. L'header add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; indica ai browser di rifiutare HTTP in chiaro per quel nome host per due anni. I browser applicano questa impostazione dalla propria cache, quindi rimuovere l'header in seguito non la annulla. Inizia con un max-age di alcune ore, verifica che ogni sottodominio utilizzi realmente HTTPS, quindi aumenta il valore.

FAQ

Qual è la differenza tra HTTP e HTTPS?

HTTPS è HTTP trasmesso all'interno di una sessione TLS (transport layer security). I metodi e i codici di stato sono identici. Cambia il fatto che i byte vengono cifrati tra il client e il componente che termina TLS, mentre la porta predefinita passa da 80 a 443. Poiché l'handshake TLS termina prima dell'invio del primo byte HTTP, un errore del certificato non produce mai un codice di stato. Per questo l'avviso del browser sul certificato mostra un nome di errore come NET::ERR_CERT_COMMON_NAME_INVALID invece di un numero come 403.

Perché il mio sito restituisce 502 Bad Gateway?

Un 502 restituito da nginx indica che nginx non ha ricevuto una risposta utilizzabile dall'upstream a cui inoltra le richieste. La richiesta del visitatore era quindi corretta, mentre qualcosa dietro nginx non lo era. Leggi /var/log/nginx/error.log. connect() failed (111: Connection refused) while connecting to upstream indica che nessun processo è in ascolto sull'indirizzo e sulla porta specificati in proxy_pass. Verifica quindi che l'applicazione sia in esecuzione e in ascolto sull'indirizzo previsto. no live upstreams while connecting to upstream indica che tutti i server nel blocco upstream sono stati contrassegnati come non disponibili dopo errori ripetuti. Confrontalo con 504 Gateway Timeout, che indica che l'upstream ha accettato la connessione ma non ha risposto entro proxy_read_timeout.

Perché il mio log degli accessi mostra lo stesso indirizzo IP per ogni visitatore?

Perché $remote_addr registra l'indirizzo che ha aperto la connessione TCP e, dietro un reverse proxy o una content delivery network, quell'indirizzo è quello del proxy. L'indirizzo del visitatore arriva invece nell'header X-Forwarded-For. Configura proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; sul proxy, quindi configura set_real_ip_from su nginx, indicando l'intervallo di indirizzi del proxy, e real_ip_header X-Forwarded-For;. Elenca solo gli intervalli che controlli, perché quell'header è testo che qualsiasi client può inviare. Considerare attendibile l'header dall'intera Internet permetterebbe a un visitatore di scegliere l'indirizzo registrato e quello usato per il rate limiting.

Devo abilitare HTTP/2 o HTTP/3?

Conviene abilitare HTTP/2, perché richiede una sola direttiva su un sito che dispone già di TLS e rimuove il limite di richieste per connessione, che rallenta le pagine con molti file di piccole dimensioni. HTTP/3 offre un vantaggio più limitato e meno certo e richiede una porta UDP 443 aperta, oltre a una build del proxy con supporto QUIC. Ricorda che i browser passano a HTTP/3 solo dopo aver ricevuto un header Alt-Svc in una risposta precedente. Senza quell'header non cambia nulla, indipendentemente dal contenuto della direttiva listen. Aggiungi $server_protocol al formato dei log e misura il protocollo effettivamente negoziato dai visitatori prima di dedicarvi tempo.

Che cosa significa 403 Forbidden quando il file esiste?

Su un sito statico, un 403 è generalmente un problema di permessi del filesystem, non una regola HTTP. open() ... failed (13: Permission denied) in /var/log/nginx/error.log indica che l'utente del worker nginx non può leggere il file. Nella maggior parte dei casi, il problema è che una directory padre non concede agli altri il permesso di esecuzione, non che la modalità del file sia errata. directory index of ... is forbidden indica che la richiesta è stata risolta in una directory priva di un file index mentre autoindex è disabilitato. Anche una regola deny esplicita nel blocco location corrispondente restituisce 403. Leggi quindi quel blocco quando il log degli errori non fornisce informazioni.

#http#https#web-server#headers#http3