Een redirect (doorverwijzing) lijkt eenvoudig: de server antwoordt met een statuscode en een Location-header, en de client gaat daarheen. Maar die drie cijfers bepalen of browsers de redirect mogen cachen, of een POST een POST blijft en welke URL zoekmachines waarschijnlijk indexeren.

Deze gids behandelt de 3xx-klasse zoals RFC 9110 die definieert, de geschiedenis achter de overlap tussen de codes, redirects zonder statuscode en hoe je de juiste code instelt op veelgebruikte servers.

Wat de 3xx-klasse betekent

RFC 9110, de huidige specificatie van de HTTP-semantiek, zegt dat een 3xx-statuscode aangeeft dat “de user-agent verdere actie moet ondernemen om het verzoek uit te voeren”. Sectie 15.4 verdeelt de codes in vier soorten redirects:

  • De resource is beschikbaar op een andere URI, die in Location staat: 301, 302, 307 en 308.
  • De client kan kiezen uit meerdere representaties van de resource: 300.
  • Een andere resource levert een indirect antwoord op het verzoek: 303.
  • De client kan een kopie hergebruiken die al in zijn cache staat: 304.

In de praktijk werken alleen 301, 302, 303, 307 en 308 als automatische redirects. De Fetch Standard, die browsers implementeren, noemt precies deze vijf codes “redirect statuses”. Een 300 wordt meestal als een gewone pagina weergegeven, en een 304 vertelt de client dat hij de kopie in zijn cache moet gebruiken.

De waarde van Location mag een relatieve verwijzing zijn, zoals /new-page of ../shop/. De client lost die op ten opzichte van de URL die hij net heeft opgevraagd (RFC 9110, sectie 10.2.2). Bevat Location geen fragment, dan blijft het oorspronkelijke fragment behouden: een link naar /guide#install die doorstuurt naar /docs/guide, eindigt op /docs/guide#install.

Voor de vijf echte redirects bepalen twee vragen welke code past:

  1. Is de verhuizing permanent of tijdelijk? Permanente redirects mogen worden gecachet, en ze geven zoekmachines het signaal om voortaan de nieuwe URL te indexeren.
  2. Moet de requestmethode behouden blijven? Dat is van belang als het oorspronkelijke verzoek een POST, PUT of DELETE is in plaats van een gewone GET.

Redirect-statuscodes in één oogopslag

Code Naam Permanent Methode behouden Standaard cachebaar Typisch gebruik Google Zoeken
300 Multiple Choices – – Ja Alternatieve representaties aanbieden (zeldzaam) –
301 Moved Permanently Ja Nee: POST kan GET worden Ja Verhuisde pagina’s en sites, HTTPS en canonieke host Permanent (sterk signaal)
302 Found Nee Nee: POST kan GET worden Nee Kortdurende redirects, loginmuren, A/B-tests Tijdelijk (zwak signaal)
303 See Other Nee Nee: wordt GET Nee Resultaatpagina na het verzenden van een formulier Tijdelijk
304 Not Modified Geen redirect – – Antwoord op een conditioneel verzoek Content ongewijzigd
305 Use Proxy Verouderd – – Niet gebruiken –
306 (niet in gebruik) Gereserveerd – – Niet gebruiken –
307 Temporary Redirect Nee Ja Nee Tijdelijke redirects voor elke methode Tijdelijk
308 Permanent Redirect Ja Ja Ja Permanente verhuizing van API’s, formulieren of elke andere URL Permanent (sterk signaal)

“Standaard cachebaar” betekent dat de code heuristisch cachebaar is volgens RFC 9110, sectie 15.1: een cache mag de response hergebruiken zonder bij de server na te vragen of die nog klopt, ook als de response geen expliciete informatie over de geldigheidsduur bevat. Met Cache-Control kun je elke response alsnog cachebaar of juist niet-cachebaar maken. De kolom Google vat de documentatie van Google over redirects en HTTP-statuscodes samen.

De codes één voor één

300 Multiple Choices

De resource heeft meerdere representaties, zoals verschillende talen of bestandsformaten, en de client mag kiezen. De body somt de opties meestal op, zonder vast formaat. Een voorkeurskeuze kan in Location worden genoemd, maar je kunt er niet op rekenen dat browsers die volgen. In de praktijk wordt 300 bijna nooit gebruikt: content negotiation op de server of een gewone pagina met links werkt beter.

301 Moved Permanently

De doelresource “heeft een nieuwe permanente URI gekregen, en alle toekomstige verwijzingen naar deze resource horen een van de meegestuurde URI’s te gebruiken”. Dit is de standaardcode voor verhuisde pagina’s, gewijzigde domeinen, upgrades van HTTP naar HTTPS en redirects naar de canonieke host.

Twee details zijn belangrijk. Een 301 is heuristisch cachebaar, dus browsers kunnen hem onthouden en de server niet meer raadplegen (zie het onderdeel over caching hieronder). En om historische redenen mag een client een POST veranderen in een GET wanneer hij een 301 volgt, en browsers doen dat ook. Voor pagina’s die met GET worden geladen, maakt dat niet uit; gebruik voor een formulierendpoint of een API 308.

302 Found

De doelresource “bevindt zich tijdelijk onder een andere URI”, dus clients moeten voor toekomstige verzoeken de oorspronkelijke URL blijven gebruiken. Een 302 is standaard niet cachebaar. Gebruik hem voor kortdurende situaties: een actiepagina die een week lang een categorie vervangt, een loginmuur, geografische routering of een A/B-test.

Net als een 301 staat een 302 toe dat een POST een GET wordt. Veel frameworks sturen een 302 als je geen code opgeeft. Daarom eindigen permanente verhuizingen zo vaak per ongeluk als 302.

303 See Other

Een 303 stuurt de client naar een andere resource die als indirect antwoord op het oorspronkelijke verzoek dient, en de client haalt die op met GET (of HEAD). De code is bedoeld voor het Post/Redirect/Get-patroon: nadat de server een verzonden formulier heeft verwerkt, antwoordt hij met een 303 en de URL van een bevestigings- of resultaatpagina. Ververst de gebruiker die pagina, dan wordt alleen een onschuldige GET herhaald en wordt het formulier niet opnieuw verzonden.

Een 303 is standaard niet cachebaar en beweert niet dat de oorspronkelijke URL is verhuisd. Google behandelt hem als een 302.

304 Not Modified (geen redirect)

Ondanks zijn 3xx-nummer stuurt een 304 de client nooit ergens heen. Hij beantwoordt een conditioneel GET- of HEAD-verzoek: een verzoek met If-None-Match en een ETag-waarde, of met If-Modified-Since en een datum. Is de resource niet gewijzigd, dan antwoordt de server met een 304 zonder body en gebruikt de client de kopie in zijn cache.

HTTP
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=3600

Redirectcheckers sturen niet-conditionele verzoeken, dus ze krijgen normaal gesproken nooit een 304 te zien. Wil je er in TrueURL toch een testen, voer dan een normale controle uit, kopieer de ETag-waarde uit de responseheaders en stuur die als If-None-Match-header mee onder Aangepaste requestheaders. Voor Googlebot betekent een 304 dat de content sinds de laatste crawl niet is gewijzigd.

305 Use Proxy en 306 (niet in gebruik)

Een 305 vertelde de client het verzoek te herhalen via de proxy die in Location stond. Dat een response de proxy-instellingen van een client kan wijzigen, is een beveiligingsrisico, dus RFC 7231 heeft de code afgeschaft, en huidige browsers doen er niets mee.

306 werd gedefinieerd in een eerdere versie van de specificatie (vaak vermeld als Switch Proxy) en is nu gereserveerd. Geen van beide codes hoort thuis in een moderne applicatie; zie je er een, dan is er iets verkeerd geconfigureerd.

307 Temporary Redirect

Een 307 betekent hetzelfde als een 302, met één toevoeging: de client mag de requestmethode niet veranderen. Een POST wordt als POST, met dezelfde body, opnieuw naar de nieuwe URL gestuurd. Daarmee is 307 de juiste tijdelijke redirect voor API’s en formulierendpoints.

Je kunt 307 ook tegenkomen in Chrome DevTools, als een 307 Internal Redirect van http:// naar https://. Die heeft de server nooit gestuurd. De browser heeft het verzoek zelf geüpgraded, meestal vanwege een HSTS-beleid dat hij voor de site heeft opgeslagen.

308 Permanent Redirect

Een 308 is de tegenhanger van 301 die de methode behoudt: permanent, heuristisch cachebaar, en de client mag de methode niet veranderen. De code werd in 2014 voor het eerst gepubliceerd in de experimentele RFC 7238, werd in 2015 een standaard met RFC 7538 en maakt nu deel uit van RFC 9110.

Gebruik 308 voor permanente verhuizingen van endpoints die POST-, PUT- of DELETE-verzoeken ontvangen. Voor gewone pagina’s zijn 301 en 308 uitwisselbaar, en Google behandelt beide als permanente redirects.

Waarom 303, 307 en 308 bestaan

De overlap tussen 301/302 en 303/307/308 is een historisch ongelukje.

HTTP/1.0 (RFC 1945, 1996) definieerde 301 Moved Permanently en 302 Moved Temporarily. Volgens die specificatie mocht een user-agent een POST niet automatisch doorsturen zonder bevestiging van de gebruiker, en er stond al in dat sommige user-agents de POST ten onrechte in een GET veranderden. Browsers bleven dat doen.

HTTP/1.1 voegde twee codes toe die geen ruimte laten voor interpretatie. RFC 2068 (1997) introduceerde 303 See Other, die altijd overschakelt op GET. RFC 2616 (1999) voegde 307 Temporary Redirect toe, die de methode altijd behoudt, hernoemde 302 tot Found en vermeldde openlijk dat de meeste user-agents een 302 als een 303 behandelden.

Daarmee ontbrak nog steeds een permanente redirect die de methode behoudt. 308 vulde dat gat vijftien jaar later. Intussen legde de specificatie zich neer bij de praktijk: sinds RFC 7231 (2014) mag een user-agent een POST na een 301 of 302 veranderen in een GET.

Tegenwoordig passen browsers deze regels uit de Fetch Standard toe:

Status Oorspronkelijke methode Browsers vragen de nieuwe URL op met
301, 302 POST GET, zonder de body
301, 302 Elke andere methode Dezelfde methode
303 Elke methode behalve GET en HEAD GET, zonder de body
303 GET of HEAD Dezelfde methode
307, 308 Elke methode Dezelfde methode en body

Alleen POST wordt na een 301 of 302 herschreven; een PUT of DELETE behoudt zijn methode. Daar kun je beter niet op bouwen, want HTTP-bibliotheken gedragen zich verschillend. Gebruik dus 307 of 308 zodra de methode ertoe doet.

Redirects zonder 3xx-statuscode

Meta refresh

HTML
<meta http-equiv="refresh" content="0; url=https://example.com/new-page">

De pagina zelf wordt geleverd met 200 OK, en de browser volgt de tag nadat de pagina is geladen. Het getal is de vertraging in seconden. Google interpreteert een directe meta refresh als een permanente redirect en een vertraagde als een tijdelijke redirect.

Getimede refreshes zijn bovendien slecht voor de toegankelijkheid. WCAG accepteert een directe meta refresh als client-side redirect (techniek H76), maar beschouwt een getimede als een fout (F40), omdat de pagina dan verandert op een timer waar gebruikers geen invloed op hebben.

JavaScript-redirects

HTML
<script>
  window.location.replace('https://example.com/new-page');
</script>

location.replace() laat de doorsturende pagina niet in de sessiegeschiedenis achter, zodat gebruikers met de terugknop niet opnieuw in de redirect belanden. Wijs je een waarde toe aan location.href, dan blijft de pagina daar wel staan.

Google voert JavaScript pas uit bij het renderen van een pagina, na het crawlen, en renderen kan mislukken. De documentatie van Google raadt JavaScript-redirects alleen aan als server-side redirects en meta refresh niet mogelijk zijn. Veel andere bots en tools voeren helemaal geen JavaScript uit.

De Refresh-header

HTTP
HTTP/1.1 200 OK
Content-Type: text/html
Refresh: 0; url=https://example.com/new-page

De Refresh-header is in geen enkele HTTP-RFC gedefinieerd. Browsers ondersteunen hem al tientallen jaren, en de HTML Standard behandelt hem als gelijkwaardig aan een meta-refresh-tag. Veel HTTP-clients en tools, waaronder curl, negeren hem, dus kies liever voor een echte 3xx-redirect.

Redirects correct instellen

nginx

nginx
# Elk http://-verzoek, op beide hosts, gaat in één hop naar de canonieke HTTPS-URL
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 en ssl_certificate_key zoals hierboven

    # Eén verhuisde pagina
    location = /old-page {
        return 301 /new-page;
    }

    # Een hele sectie, met behoud van de rest van het pad
    location /blog/ {
        rewrite ^/blog/(.*)$ /articles/$1 permanent;
    }
}

$request_uri bevat het oorspronkelijke pad en de querystring, dus er gaat niets verloren. rewrite ... permanent stuurt een 301 en rewrite ... redirect een 302; gebruik voor 307 of 308 return met die code.

Apache (.htaccess)

Apache
# mod_alias: één pagina en een hele map (de rest van het pad wordt toegevoegd)
Redirect 301 /old-page https://example.com/new-page
Redirect 301 /blog/ https://example.com/articles/

# mod_alias met een reguliere expressie
RedirectMatch 301 ^/products/([0-9]+)\.html$ https://example.com/p/$1

# mod_rewrite: stuur elk http://- en www-verzoek in één hop naar https://example.com
RewriteEngine On
RewriteCond %{HTTPS} off [OR]
RewriteCond %{HTTP_HOST} ^www\. [NC]
RewriteRule ^ https://example.com%{REQUEST_URI} [R=301,L]

Redirect zonder statuscode stuurt een 302, dus vermeld de code altijd. Achter een load balancer of CDN die via gewoon HTTP met Apache verbindt, staat %{HTTPS} altijd op off en raakt de laatste regel in een loop; test in plaats daarvan %{HTTP:X-Forwarded-Proto}. mod_alias en mod_rewrite werken onafhankelijk van elkaar, en de volgorde waarin de regels in het bestand staan, bepaalt niet welke module als eerste draait. Een URL die op regels van beide modules past, zoals hier http://www.example.com/old-page, kan twee hops nodig hebben. Houd voor URL’s die in één hop moeten doorsturen al hun regels in één module.

Cloudflare

Cloudflare kan al aan de edge doorsturen, nog voordat een verzoek je server bereikt:

  • Single Redirects (het onderdeel Redirect Rules in het dashboard) selecteren verzoeken met een filterexpressie en sturen ze met 301, 302, 303, 307 of 308 door naar een statisch of dynamisch doel. Een typische regel van www naar het kale domein (apex) heeft deze instellingen:

    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:         On
  • Bulk Redirects verwerken lange lijsten met bron- en doel-URL’s, zoals een migratietabel die als CSV is geüpload. Elk item heeft een eigen statuscode en opties zoals het behouden van de querystring, en een Bulk Redirect Rule activeert de lijst.

  • Workers kunnen redirects in code opbouwen met Response.redirect():

JavaScript
export default {
  async fetch(request) {
    const url = new URL(request.url);
    if (url.pathname === '/old-page') {
      url.pathname = '/new-page'; // de querystring blijft behouden
      return Response.redirect(url.toString(), 301);
    }
    return fetch(request);
  },
};

Response.redirect() gebruikt standaard 302 en accepteert alleen 301, 302, 303, 307 en 308. Elke andere code levert een RangeError op.

Node.js en Express

JavaScript
import express from 'express';

const app = express();
app.set('trust proxy', 1); // als de app achter een load balancer of CDN draait

// HTTPS en de canonieke host in één hop
app.use((req, res, next) => {
  if (req.secure && req.hostname === 'example.com') return next();
  res.redirect(301, `https://example.com${req.originalUrl}`);
});

// Eén verhuisde pagina
app.get('/old-page', (req, res) => {
  res.redirect(301, '/new-page');
});

res.redirect(url) zonder status stuurt een 302. Express 5 ondersteunt alleen de volgorde res.redirect(status, url). In gewoon Node.js stel je de status en de header zelf in:

JavaScript
res.writeHead(301, { Location: 'https://example.com/new-page' });
res.end();

PHP

PHP
<?php
// Moet draaien voordat er output wordt verstuurd.
header('Location: https://example.com/new-page', true, 301);
exit;

Met alleen een Location-header stuurt PHP een 302 (tenzij er al een 201- of 3xx-status was ingesteld). Geef de code dus mee als derde argument, en roep exit aan zodat er daarna niets meer wordt uitgevoerd. In WordPress gebruiken wp_redirect() en wp_safe_redirect() ook standaard 302; geef 301 mee als tweede argument.

De juiste redirectcode kiezen

  • De pagina is definitief verhuisd en het is een gewone pagina die met GET wordt geladen: gebruik 301. Een 308 werkt net zo goed.
  • De verhuizing is permanent en clients kunnen POST, PUT of DELETE sturen, zoals bij API’s, formulieracties en webhooks: gebruik 308.
  • De redirect is tijdelijk en alleen GET telt: gebruik 302.
  • De redirect is tijdelijk en de methode moet behouden blijven: gebruik 307.
  • Een formulier of andere POST is verwerkt en de gebruiker moet een resultaatpagina zien: gebruik 303.
  • HTTP naar HTTPS: gebruik 301 of 308, en stuur vanaf de HTTPS-site een Strict-Transport-Security-header mee, zodat browsers het onveilige verzoek de volgende keer overslaan.
  • De content is weg en heeft geen equivalent: stuur niet door. Geef 404 of 410 terug. Google kan een redirect naar een niet-verwante pagina, zoals de homepage, als een soft 404 behandelen.
  • 300, 305 en 306: gebruik ze niet. Een 304 wordt door je server of cache aangemaakt bij conditionele verzoeken en is nooit een keuze voor een redirect.

De cachingvalkuil van 301 en 308

Omdat 301 en 308 heuristisch cachebaar zijn, kunnen browsers ze opslaan en de server niet meer raadplegen. Browsers op basis van Chromium bewaren een 301 of 308 zonder cachingheaders tot het item uit de cache wordt verdrongen of de gebruiker de cache wist. Een verkeerde permanente redirect kan eerdere bezoekers dus nog lang naar de verkeerde plek sturen nadat je de server hebt gerepareerd, en je kunt hem niet betrouwbaar uit hun browsers terughalen.

Zo blijf je aan de veilige kant:

  • Test nieuwe regels eerst met 302 of 307, en stap over op 301 of 308 zodra de bestemming vaststaat.
  • Of beperk tijdens de uitrol hoelang de permanente redirect mag worden gecachet:
nginx
location = /old-page {
    add_header Cache-Control "max-age=3600";
    return 301 https://example.com/new-page;
}
  • Gebruik bij het testen een privévenster of DevTools met Disable cache, of een server-side checker, die nooit een browsercache gebruikt.
  • Leeg de caches van je CDN en proxy’s nadat je een redirect hebt gewijzigd, want ook die kunnen redirects opslaan.
  • Probeer een gecachete redirect A → B niet ongedaan te maken door B terug te sturen naar A. Bezoekers van wie de browser de eerste redirect heeft gecachet, kunnen dan in een loop belanden.

Je redirects testen

Op welk platform je ook werkt, controleer het resultaat altijd van buitenaf. De 301-redirectchecker bevestigt dat elke hop van een permanente verhuizing een 301 of 308 is, de Redirectketen-checker toont elke hop met zijn Location en headers, en de HTTPS-redirectchecker test alle vier varianten van protocol en host tegelijk. De Meta-refresh-checker vindt client-side redirects die ontsnappen aan tools die alleen headers lezen, en de Redirectlatentie-checker meet wat elke hop kost.

Vanuit een terminal toont curl -sIL https://example.com/old-page de headers van elke hop. Onze gids over URL-redirects controleren behandelt de commandline, DevTools en veelvoorkomende redirectproblemen in detail.