Un reindirizzamento sembra semplice: il server risponde con un codice di stato e un’intestazione Location, e il client va a quell’indirizzo. Ma sono quelle tre cifre a decidere se i browser possono memorizzare il reindirizzamento nella cache, se un POST resta un POST e quale URL verrà probabilmente indicizzato dai motori di ricerca.
Questa guida illustra la classe 3xx così come la definisce la RFC 9110, la storia che spiega le sue sovrapposizioni, i reindirizzamenti senza codice di stato e come configurare il codice giusto sui server più diffusi.
Che cosa indica la classe 3xx
La RFC 9110, la specifica attuale della semantica di HTTP, afferma che un codice di stato 3xx indica che “per soddisfare la richiesta è necessaria un’ulteriore azione da parte dello user agent”. La sezione 15.4 raggruppa i codici in quattro tipi di reindirizzamento:
- La risorsa è disponibile a un altro URI, indicato in
Location: 301, 302, 307 e 308. - Il client può scegliere tra più rappresentazioni della risorsa: 300.
- Una risorsa diversa fornisce una risposta indiretta alla richiesta: 303.
- Il client può riutilizzare una copia che ha già nella cache: 304.
In pratica, solo 301, 302, 303, 307 e 308 funzionano come reindirizzamenti automatici. Il Fetch Standard, implementato dai browser, chiama proprio questi cinque codici “redirect status” (stati di reindirizzamento). Un 300 di solito viene visualizzato come una normale pagina, mentre un 304 dice al client di usare la copia presente nella cache.
Il valore di Location può essere un riferimento relativo, come /new-page o ../shop/. Il client lo risolve rispetto all’URL appena richiesto (RFC 9110, sezione 10.2.2). Se Location non contiene un frammento, viene mantenuto quello originale: un link a /guide#install che reindirizza a /docs/guide termina su /docs/guide#install.
Per i cinque veri reindirizzamenti, la scelta del codice dipende da due domande:
- Lo spostamento è permanente o temporaneo? I reindirizzamenti permanenti possono essere memorizzati nella cache e indicano ai motori di ricerca di indicizzare il nuovo URL al posto di quello vecchio.
- Il metodo della richiesta va mantenuto? Conta quando la richiesta originale è un POST, un PUT o un DELETE e non un semplice GET.
I codici di stato dei redirect in sintesi
| Codice | Nome | Permanente | Mantiene il metodo | In cache di default | Uso tipico | Google Search |
|---|---|---|---|---|---|---|
| 300 | Multiple Choices | – | – | Sì | Offrire rappresentazioni alternative (raro) | – |
| 301 | Moved Permanently | Sì | No: POST può diventare GET | Sì | Pagine e siti spostati, canonicalizzazione di HTTPS e host | Permanente (segnale forte) |
| 302 | Found | No | No: POST può diventare GET | No | Reindirizzamenti di breve durata, login wall, test A/B | Temporaneo (segnale debole) |
| 303 | See Other | No | No: diventa GET | No | Pagina di risultato dopo l’invio di un modulo | Temporaneo |
| 304 | Not Modified | Non è un redirect | – | – | Risposta a una richiesta condizionale | Contenuto invariato |
| 305 | Use Proxy | Deprecato | – | – | Da non usare | – |
| 306 | (Non utilizzato) | Riservato | – | – | Da non usare | – |
| 307 | Temporary Redirect | No | Sì | No | Reindirizzamenti temporanei con qualsiasi metodo | Temporaneo |
| 308 | Permanent Redirect | Sì | Sì | Sì | Spostamenti permanenti di API, moduli o qualsiasi URL | Permanente (segnale forte) |
“In cache di default” significa che il codice è memorizzabile in cache in modo euristico secondo la RFC 9110, sezione 15.1: una cache può riutilizzare la risposta senza consultare il server, anche quando la risposta non contiene informazioni esplicite sulla sua validità (freshness). Con Cache-Control si può comunque rendere qualsiasi risposta memorizzabile o non memorizzabile in cache. La colonna Google riassume la documentazione di Google sui reindirizzamenti e sui codici di stato HTTP.
I codici uno per uno
300 Multiple Choices
La risorsa ha più rappresentazioni, ad esempio in lingue o formati di file diversi, e il client può scegliere. Di solito il corpo della risposta elenca le opzioni, senza un formato standard. In Location si può indicare la scelta preferita, ma non puoi contare sul fatto che i browser la seguano. In pratica il 300 non si usa quasi mai: la negoziazione dei contenuti lato server o una normale pagina con dei link funzionano meglio.
301 Moved Permanently
Alla risorsa di destinazione “è stato assegnato un nuovo URI permanente e qualsiasi riferimento futuro a questa risorsa dovrebbe usare uno degli URI forniti”. È il codice standard per pagine spostate, cambi di dominio, passaggi da HTTP a HTTPS e reindirizzamenti verso l’host canonico.
Due dettagli sono importanti. Un 301 è memorizzabile in cache in modo euristico, quindi i browser possono ricordarlo e smettere di interrogare il server (vedi la sezione sulla cache più avanti). Inoltre, per ragioni storiche, un client può trasformare un POST in GET quando segue un 301, ed è proprio ciò che fanno i browser. Per le pagine caricate con GET non fa differenza; per l’endpoint di un modulo o per un’API, usa il 308.
302 Found
La risorsa di destinazione “si trova temporaneamente a un URI diverso”, quindi i client dovrebbero continuare a usare l’URL originale per le richieste future. Un 302 non è memorizzabile in cache di default. Usalo per situazioni di breve durata: una pagina dei saldi che sostituisce una categoria per una settimana, un login wall, l’instradamento in base alla posizione geografica o un test A/B.
Come il 301, anche il 302 consente il passaggio da POST a GET. Molti framework inviano un 302 quando non viene indicato alcun codice, ed è per questo che tanti spostamenti permanenti finiscono per errore con un 302.
303 See Other
Un 303 manda il client verso una risorsa diversa, che funge da risposta indiretta alla richiesta originale e che il client recupera con GET (o HEAD). È pensato per il pattern Post/Redirect/Get: dopo aver elaborato l’invio di un modulo, il server risponde 303 con l’URL di una pagina di conferma o di risultato. Se poi l’utente ricarica quella pagina, viene ripetuta un’innocua richiesta GET invece di inviare di nuovo il modulo.
Un 303 non è memorizzabile in cache di default e non afferma che l’URL originale sia stato spostato. Google lo tratta come un 302.
304 Not Modified (non è un reindirizzamento)
Anche se il suo numero lo colloca tra i 3xx, un 304 non manda mai il client da nessuna parte. Risponde a una richiesta GET o HEAD condizionale, cioè che contiene If-None-Match con un valore ETag oppure If-Modified-Since con una data. Se la risorsa non è cambiata, il server risponde 304 senza corpo e il client usa la copia presente nella cache.
GET /styles.css HTTP/1.1
Host: example.com
If-None-Match: "5f1c-3a2b"
HTTP/1.1 304 Not Modified
ETag: "5f1c-3a2b"
Cache-Control: max-age=3600I redirect checker inviano richieste non condizionali, quindi di norma non vedono mai un 304. Per testarne uno in TrueURL, esegui una verifica normale, copia il valore ETag dalle intestazioni della risposta e invialo come intestazione If-None-Match in Intestazioni di richiesta personalizzate. Per Googlebot, un 304 significa che il contenuto non è cambiato dall’ultima scansione.
305 Use Proxy e 306 (non utilizzato)
Un 305 chiedeva al client di ripetere la richiesta tramite il proxy indicato in Location. Permettere a una risposta di riconfigurare il proxy di un client è un rischio per la sicurezza, quindi la RFC 7231 ha deprecato il codice e i browser attuali lo ignorano.
Il 306 era definito in una versione precedente della specifica (spesso compare come Switch Proxy) e ora è riservato. Nessuno dei due codici ha posto in un’applicazione moderna: se ne vedi uno, c’è qualcosa di configurato male.
307 Temporary Redirect
Un 307 ha lo stesso significato di un 302, con un’aggiunta: il client non deve cambiare il metodo della richiesta. Un POST viene reinviato al nuovo URL come POST, con lo stesso corpo. Per questo il 307 è il reindirizzamento temporaneo giusto per le API e gli endpoint dei moduli.
Nei DevTools di Chrome puoi incontrare il 307 anche come 307 Internal Redirect da http:// a https://. Il server non l’ha mai inviato. È il browser ad aver convertito da solo la richiesta in HTTPS, di solito per via di una policy HSTS che ha memorizzato per il sito.
308 Permanent Redirect
Il 308 è l’equivalente del 301 che preserva il metodo: è permanente, è memorizzabile in cache in modo euristico e il client non deve cambiare il metodo. È stato pubblicato per la prima volta nel 2014 nella RFC 7238, di tipo sperimentale, è diventato uno standard con la RFC 7538 nel 2015 e oggi fa parte della RFC 9110.
Usa il 308 per gli spostamenti permanenti di endpoint che ricevono richieste POST, PUT o DELETE. Per le pagine normali, 301 e 308 sono intercambiabili e Google li tratta entrambi come reindirizzamenti permanenti.
Perché esistono 303, 307 e 308
La sovrapposizione tra 301/302 e 303/307/308 è un incidente della storia.
HTTP/1.0 (RFC 1945, 1996) definiva 301 Moved Permanently e 302 Moved Temporarily. Stabiliva che uno user agent non dovesse reindirizzare automaticamente un POST senza la conferma dell’utente, e già notava che alcuni user agent trasformavano erroneamente il POST in GET. I browser hanno continuato a farlo.
HTTP/1.1 ha aggiunto due codici che non lasciano spazio a interpretazioni. La RFC 2068 (1997) ha introdotto 303 See Other, che passa sempre a GET. La RFC 2616 (1999) ha aggiunto 307 Temporary Redirect, che mantiene sempre il metodo, ha rinominato il 302 in Found e ha riconosciuto apertamente che la maggior parte degli user agent trattava un 302 come un 303.
Mancava però ancora un reindirizzamento permanente che mantenesse il metodo. Il 308 ha colmato la lacuna quindici anni dopo. Nel frattempo la specifica si è adeguata alla realtà: dalla RFC 7231 (2014), uno user agent può trasformare un POST in GET dopo un 301 o un 302.
Oggi i browser applicano queste regole del Fetch Standard:
| Codice | Metodo originale | I browser richiedono il nuovo URL con |
|---|---|---|
| 301, 302 | POST | GET, senza il corpo |
| 301, 302 | Qualsiasi altro metodo | Lo stesso metodo |
| 303 | Qualsiasi metodo tranne GET e HEAD | GET, senza il corpo |
| 303 | GET o HEAD | Lo stesso metodo |
| 307, 308 | Qualsiasi metodo | Lo stesso metodo e lo stesso corpo |
Dopo un 301 o un 302 viene riscritto solo il POST; un PUT o un DELETE mantiene il proprio metodo. Affidarsi a questo comportamento è rischioso, perché le librerie HTTP non si comportano tutte allo stesso modo: usa 307 o 308 ogni volta che il metodo conta.
Redirect senza codice di stato 3xx
Meta refresh
<meta http-equiv="refresh" content="0; url=https://example.com/new-page">La pagina stessa viene servita con 200 OK e il browser segue il tag dopo averla caricata. Il numero indica il ritardo in secondi. Google interpreta un meta refresh immediato come reindirizzamento permanente e uno ritardato come reindirizzamento temporaneo.
I refresh temporizzati penalizzano anche l’accessibilità. Le WCAG accettano un meta refresh immediato come reindirizzamento lato client (tecnica H76), ma considerano un errore quello temporizzato (F40), perché la pagina cambia allo scadere di un timer che l’utente non può controllare.
Redirect JavaScript
<script>
window.location.replace('https://example.com/new-page');
</script>location.replace() non lascia la pagina che reindirizza nella cronologia della sessione, quindi il pulsante Indietro non riporta l’utente dentro il reindirizzamento. Assegnando un valore a location.href, invece, la pagina resta nella cronologia.
Google esegue JavaScript solo in fase di rendering di una pagina, che avviene dopo la scansione e può non riuscire. La sua documentazione consiglia i reindirizzamenti JavaScript solo quando non sono possibili reindirizzamenti lato server o meta refresh. Molti altri bot e strumenti non eseguono mai JavaScript.
L’intestazione Refresh
HTTP/1.1 200 OK
Content-Type: text/html
Refresh: 0; url=https://example.com/new-pageL’intestazione Refresh non è definita in nessuna RFC di HTTP. I browser la supportano da decenni e l’HTML Standard la considera equivalente a un tag meta refresh. Molti client e strumenti HTTP, tra cui curl, la ignorano, quindi è preferibile un vero reindirizzamento 3xx.
Come implementare correttamente i redirect
nginx
# Ogni richiesta http://, su entrambi gli host, va all’URL HTTPS canonico con un solo salto
server {
listen 80;
server_name example.com www.example.com;
return 301 https://example.com$request_uri;
}
# https://www.example.com -> https://example.com
server {
listen 443 ssl;
server_name www.example.com;
ssl_certificate /etc/ssl/example.com.crt;
ssl_certificate_key /etc/ssl/example.com.key;
return 301 https://example.com$request_uri;
}
server {
listen 443 ssl;
server_name example.com;
# ssl_certificate e ssl_certificate_key come sopra
# Una pagina spostata
location = /old-page {
return 301 /new-page;
}
# Un’intera sezione, mantenendo il resto del percorso
location /blog/ {
rewrite ^/blog/(.*)$ /articles/$1 permanent;
}
}$request_uri contiene il percorso e la query string originali, quindi non si perde nulla. rewrite ... permanent invia un 301 e rewrite ... redirect un 302; per un 307 o un 308, usa return con il codice corrispondente.
Apache (.htaccess)
# mod_alias: una pagina e un’intera cartella (il resto del percorso viene aggiunto)
Redirect 301 /old-page https://example.com/new-page
Redirect 301 /blog/ https://example.com/articles/
# mod_alias con un’espressione regolare
RedirectMatch 301 ^/products/([0-9]+)\.html$ https://example.com/p/$1
# mod_rewrite: invia ogni richiesta http:// e www a https://example.com con un solo salto
RewriteEngine On
RewriteCond %{HTTPS} off [OR]
RewriteCond %{HTTP_HOST} ^www\. [NC]
RewriteRule ^ https://example.com%{REQUEST_URI} [R=301,L]Redirect senza codice di stato invia un 302, quindi indica sempre il codice. Dietro un load balancer o una CDN che si collega ad Apache in HTTP in chiaro, %{HTTPS} è sempre off e l’ultima regola genera un loop: controlla invece %{HTTP:X-Forwarded-Proto}. mod_alias e mod_rewrite funzionano in modo indipendente, e l’ordine delle righe nel file non stabilisce quale modulo venga eseguito per primo. Un URL che corrisponde a regole di entrambi, come http://www.example.com/old-page in questo esempio, può richiedere due salti; per gli URL che devono reindirizzare con un solo salto, tieni tutte le regole che li riguardano in un unico modulo.
Cloudflare
Cloudflare può reindirizzare direttamente sulla rete edge, prima che la richiesta raggiunga il tuo server:
I Single Redirects (la sezione Redirect Rules della dashboard) selezionano le richieste con un’espressione di filtro e le inviano a una destinazione statica o dinamica con 301, 302, 303, 307 o 308. Una tipica regola da www al dominio principale (apex) ha queste impostazioni:
When incoming requests match: (http.host eq "www.example.com") URL redirect type: Dynamic Expression: concat("https://example.com", http.request.uri.path) Status code: 301 Preserve query string: OnI Bulk Redirects gestiscono lunghi elenchi di URL di origine e di destinazione, come una mappa di migrazione caricata in formato CSV. Ogni voce ha il proprio codice di stato e opzioni come la conservazione della query string, e l’elenco viene attivato da una Bulk Redirect Rule.
I Workers possono generare reindirizzamenti nel codice con
Response.redirect():
export default {
async fetch(request) {
const url = new URL(request.url);
if (url.pathname === '/old-page') {
url.pathname = '/new-page'; // la query string viene mantenuta
return Response.redirect(url.toString(), 301);
}
return fetch(request);
},
};Response.redirect() usa 302 come valore predefinito e accetta solo 301, 302, 303, 307 e 308. Qualsiasi altro codice genera un RangeError.
Node.js ed Express
import express from 'express';
const app = express();
app.set('trust proxy', 1); // se l’app gira dietro un load balancer o una CDN
// HTTPS e host canonico con un solo salto
app.use((req, res, next) => {
if (req.secure && req.hostname === 'example.com') return next();
res.redirect(301, `https://example.com${req.originalUrl}`);
});
// Una pagina spostata
app.get('/old-page', (req, res) => {
res.redirect(301, '/new-page');
});res.redirect(url) senza codice di stato invia un 302. Express 5 supporta solo l’ordine res.redirect(status, url). In Node.js puro imposta tu il codice di stato e l’intestazione:
res.writeHead(301, { Location: 'https://example.com/new-page' });
res.end();PHP
<?php
// Va eseguito prima di inviare qualsiasi output.
header('Location: https://example.com/new-page', true, 301);
exit;Un’intestazione Location da sola fa sì che PHP invii un 302 (a meno che non sia già stato impostato un codice 201 o 3xx), quindi passa il codice come terzo argomento e chiama exit, così non viene eseguito nient’altro. Anche in WordPress wp_redirect() e wp_safe_redirect() usano 302 come valore predefinito: passa 301 come secondo argomento.
Come scegliere il codice di redirect giusto
- La pagina si è spostata definitivamente ed è una normale pagina caricata con GET: usa il 301. Un 308 va altrettanto bene.
- Lo spostamento è permanente e i client possono inviare POST, PUT o DELETE, come nel caso di API, destinazioni dei moduli e webhook: usa il 308.
- Il reindirizzamento è temporaneo e conta solo GET: usa il 302.
- Il reindirizzamento è temporaneo e il metodo va mantenuto: usa il 307.
- È stato elaborato un modulo o un altro POST e l’utente deve vedere una pagina di risultato: usa il 303.
- Da HTTP a HTTPS: usa il 301 o il 308 e invia un’intestazione
Strict-Transport-Securitydal sito HTTPS, così la volta successiva i browser salteranno la richiesta non sicura. - Il contenuto non esiste più e non ha un equivalente: non reindirizzare. Restituisci 404 o 410. Google può trattare come soft 404 un reindirizzamento verso una pagina non pertinente, come la home page.
- 300, 305 e 306: non usarli. Un 304 viene generato dal tuo server o dalla cache in risposta alle richieste condizionali e non è mai un codice da scegliere per un reindirizzamento.
La trappola della cache con 301 e 308
Poiché 301 e 308 sono memorizzabili in cache in modo euristico, i browser possono salvarli e smettere di interrogare il server. I browser basati su Chromium conservano un 301 o un 308 privo di intestazioni di caching finché la voce non viene eliminata o l’utente non svuota la cache. Un reindirizzamento permanente errato può continuare a mandare nel posto sbagliato chi ha già visitato il sito, anche molto tempo dopo che hai corretto il server, e non c’è un modo affidabile per ritirarlo dai loro browser.
Per andare sul sicuro:
- Prova le nuove regole prima con un 302 o un 307 e passa a 301 o 308 solo quando la destinazione è confermata.
- In alternativa, limita per quanto tempo il reindirizzamento permanente può restare in cache mentre lo metti in produzione:
location = /old-page {
add_header Cache-Control "max-age=3600";
return 301 https://example.com/new-page;
}- Durante i test usa una finestra di navigazione privata o i DevTools con Disable cache, oppure uno strumento di verifica lato server, che non usa mai la cache del browser.
- Dopo aver modificato un reindirizzamento, svuota le cache della CDN e dei proxy, perché anche loro possono memorizzare i reindirizzamenti.
- Non cercare di annullare un reindirizzamento A → B già in cache reindirizzando B di nuovo verso A. I visitatori il cui browser ha memorizzato il primo reindirizzamento possono finire in un loop.
Come testare i tuoi redirect
Qualunque piattaforma tu usi, verifica il risultato dall’esterno. La Verifica redirect 301 controlla che ogni salto di uno spostamento permanente sia un 301 o un 308, la Verifica catena di redirect mostra ogni salto con la relativa intestazione Location e le altre intestazioni, e la Verifica redirect HTTPS testa contemporaneamente tutte e quattro le varianti di protocollo e host. Con la Verifica meta refresh trovi i reindirizzamenti lato client che sfuggono agli strumenti limitati alle intestazioni, mentre il Test latenza redirect misura quanto costa ogni salto.
Da terminale, curl -sIL https://example.com/old-page stampa le intestazioni di ogni salto. La nostra guida su come verificare i redirect di un URL approfondisce la riga di comando, i DevTools e i problemi più comuni con i redirect.