Przekierowanie wydaje się proste: serwer odpowiada kodem odpowiedzi i nagłówkiem Location, a klient przechodzi pod wskazany adres. Jednak to właśnie te trzy cyfry decydują o tym, czy przeglądarki mogą zapisać przekierowanie w pamięci podręcznej, czy żądanie POST pozostanie żądaniem POST i który adres URL wyszukiwarki najprawdopodobniej zindeksują.

W tym przewodniku omawiamy klasę 3xx zgodnie z definicją w RFC 9110, historię, która stoi za nakładaniem się tych kodów, przekierowania bez kodu odpowiedzi oraz konfigurowanie właściwego kodu na popularnych serwerach.

Co oznacza klasa 3xx

RFC 9110, aktualna specyfikacja semantyki HTTP, stwierdza, że kod 3xx oznacza, iż „agent użytkownika musi podjąć dalsze działania, aby zrealizować żądanie”. Sekcja 15.4 dzieli te kody na cztery rodzaje przekierowań:

  • Zasób jest dostępny pod innym URI, podanym w Location: 301, 302, 307 i 308.
  • Klient może wybrać jedną z kilku reprezentacji zasobu: 300.
  • Inny zasób dostarcza pośrednią odpowiedź na żądanie: 303.
  • Klient może ponownie użyć kopii, którą ma już w pamięci podręcznej: 304.

W praktyce jako automatyczne przekierowania działają tylko 301, 302, 303, 307 i 308. Fetch Standard, który implementują przeglądarki, nazywa właśnie tych pięć kodów „redirect statuses”, czyli statusami przekierowań. Kod 300 jest zwykle wyświetlany jak normalna strona, a 304 każe klientowi użyć kopii z pamięci podręcznej.

Wartość Location może być odwołaniem względnym, np. /new-page lub ../shop/. Klient rozwiązuje je względem adresu URL, o który właśnie poprosił (RFC 9110, sekcja 10.2.2). Jeśli Location nie zawiera fragmentu, zachowywany jest fragment pierwotny: link do /guide#install, który przekierowuje na /docs/guide, kończy się na /docs/guide#install.

W przypadku pięciu prawdziwych przekierowań o wyborze kodu decydują dwa pytania:

  1. Czy przeniesienie jest stałe, czy tymczasowe? Przekierowania stałe mogą być zapisywane w pamięci podręcznej i są dla wyszukiwarek sygnałem, by zamiast starego adresu indeksowały nowy.
  2. Czy metoda żądania musi zostać zachowana? Ma to znaczenie, gdy pierwotne żądanie to POST, PUT lub DELETE, a nie zwykły GET.

Kody przekierowań w skrócie

Kod Nazwa Stałe Metoda zachowana Domyślne buforowanie Typowe zastosowanie Wyszukiwarka Google
300 Multiple Choices – – Tak Oferowanie alternatywnych reprezentacji (rzadko) –
301 Moved Permanently Tak Nie: POST może zmienić się w GET Tak Przeniesione strony i witryny, przejście na HTTPS i kanonikalizacja hosta Stałe (silny sygnał)
302 Found Nie Nie: POST może zmienić się w GET Nie Krótkotrwałe przekierowania, wymuszanie logowania, testy A/B Tymczasowe (słaby sygnał)
303 See Other Nie Nie: zmienia się w GET Nie Strona z wynikiem po wysłaniu formularza Tymczasowe
304 Not Modified To nie przekierowanie – – Odpowiedź na żądanie warunkowe Treść bez zmian
305 Use Proxy Wycofany – – Nie używać –
306 (Unused) Zarezerwowany – – Nie używać –
307 Temporary Redirect Nie Tak Nie Tymczasowe przekierowania dowolnej metody Tymczasowe
308 Permanent Redirect Tak Tak Tak Stałe przeniesienia API, formularzy lub dowolnego adresu URL Stałe (silny sygnał)

„Domyślne buforowanie” oznacza, że odpowiedź z tym kodem może być heurystycznie zapisywana w pamięci podręcznej (heuristically cacheable) zgodnie z RFC 9110, sekcja 15.1: pamięć podręczna może ponownie użyć takiej odpowiedzi bez sprawdzania jej na serwerze, nawet jeśli odpowiedź nie zawiera jawnych informacji o świeżości. Za pomocą Cache-Control każdą odpowiedź można jednak oznaczyć jako możliwą lub niemożliwą do zapisania w pamięci podręcznej. Kolumna „Wyszukiwarka Google” streszcza dokumentację Google dotyczącą przekierowań i kodów odpowiedzi HTTP.

Kod po kodzie

300 Multiple Choices

Zasób ma kilka reprezentacji, np. w różnych językach lub formatach plików, a klient może wybrać jedną z nich. Treść odpowiedzi zwykle wymienia dostępne opcje, ale bez żadnego standardowego formatu. Preferowaną opcję można wskazać w Location, jednak nie można liczyć na to, że przeglądarki za nią podążą. W praktyce kod 300 prawie nigdy nie jest używany: lepiej sprawdza się negocjacja treści po stronie serwera albo zwykła strona z linkami.

301 Moved Permanently

Zasób docelowy „otrzymał nowy, stały URI, a wszystkie przyszłe odwołania do niego powinny używać jednego z załączonych URI”. To standardowy kod dla przeniesionych stron, zmienionych domen, przejścia z HTTP na HTTPS i przekierowań na kanoniczny host.

Ważne są dwa szczegóły. Kod 301 może być heurystycznie zapisywany w pamięci podręcznej, więc przeglądarki mogą go zapamiętać i przestać pytać serwer (zobacz sekcję o pamięci podręcznej poniżej). Poza tym ze względów historycznych klient, podążając za 301, może zmienić POST na GET – i przeglądarki tak robią. W przypadku stron ładowanych metodą GET nie ma to znaczenia; dla endpointu formularza lub API użyj 308.

302 Found

Zasób docelowy „tymczasowo znajduje się pod innym URI”, więc klienci powinni nadal używać pierwotnego adresu URL w kolejnych żądaniach. Kod 302 domyślnie nie jest zapisywany w pamięci podręcznej. Stosuj go w sytuacjach krótkotrwałych: strona promocji, która na tydzień zastępuje kategorię, wymuszenie logowania, kierowanie według lokalizacji lub test A/B.

Podobnie jak 301, kod 302 dopuszcza zmianę POST na GET. Wiele frameworków wysyła 302, gdy nie podano kodu – dlatego przeniesienia na stałe tak często przez przypadek zostają skonfigurowane jako 302.

303 See Other

Kod 303 kieruje klienta do innego zasobu, który stanowi pośrednią odpowiedź na pierwotne żądanie, a klient pobiera go metodą GET (lub HEAD). Zaprojektowano go z myślą o wzorcu Post/Redirect/Get: po przetworzeniu przesłanego formularza serwer odpowiada kodem 303 z adresem URL strony z potwierdzeniem lub wynikiem. Odświeżenie tej strony powtarza wtedy nieszkodliwe żądanie GET, zamiast ponownie wysyłać formularz.

Kod 303 domyślnie nie jest zapisywany w pamięci podręcznej i nie oznacza, że pierwotny adres URL został przeniesiony. Google traktuje go jak 302.

304 Not Modified (to nie przekierowanie)

Mimo numeru z klasy 3xx kod 304 nigdy nikogo nigdzie nie kieruje. Jest odpowiedzią na warunkowe żądanie GET lub HEAD, czyli takie, które zawiera nagłówek If-None-Match z wartością ETag albo If-Modified-Since z datą. Jeśli zasób się nie zmienił, serwer odpowiada kodem 304 bez treści, a klient korzysta z kopii w pamięci podręcznej.

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

Narzędzia do sprawdzania przekierowań (redirect checkery) wysyłają żądania bezwarunkowe, więc zwykle nigdy nie widzą kodu 304. Aby przetestować go w TrueURL, uruchom zwykłe sprawdzanie, skopiuj wartość ETag z nagłówków odpowiedzi i wyślij ją jako nagłówek If-None-Match w sekcji Własne nagłówki żądania. Dla Googlebota kod 304 oznacza, że treść nie zmieniła się od ostatniego skanowania.

305 Use Proxy i 306 (Unused)

Kod 305 nakazywał klientowi powtórzyć żądanie przez serwer proxy wskazany w Location. Możliwość zmiany ustawień proxy klienta przez odpowiedź serwera to zagrożenie dla bezpieczeństwa, dlatego w RFC 7231 uznano ten kod za przestarzały, a współczesne przeglądarki go ignorują.

Kod 306 był zdefiniowany we wcześniejszej wersji specyfikacji (często występuje pod nazwą Switch Proxy), a obecnie jest zarezerwowany. Żaden z tych kodów nie ma racji bytu we współczesnej aplikacji; jeśli któryś z nich zobaczysz, coś jest źle skonfigurowane.

307 Temporary Redirect

Kod 307 oznacza to samo co 302, z jednym dodatkiem: klientowi nie wolno zmienić metody żądania. POST zostaje ponownie wysłany jako POST, z tą samą treścią, pod nowy adres URL. Dlatego 307 to właściwe przekierowanie tymczasowe dla API i endpointów formularzy.

Kod 307 możesz też spotkać w Chrome DevTools jako 307 Internal Redirect z http:// na https://. Serwer nigdy go nie wysłał. To przeglądarka sama zmieniła żądanie na HTTPS, zwykle z powodu zapisanej dla tej witryny polityki HSTS.

308 Permanent Redirect

Kod 308 to odpowiednik 301, który zachowuje metodę: jest stały, może być heurystycznie zapisywany w pamięci podręcznej, a klientowi nie wolno zmienić metody. Po raz pierwszy opublikowano go w 2014 roku w eksperymentalnym RFC 7238, standardem stał się w 2015 roku wraz z RFC 7538, a dziś jest częścią RFC 9110.

Używaj 308 przy stałym przenoszeniu endpointów, które przyjmują żądania POST, PUT lub DELETE. W przypadku zwykłych stron 301 i 308 są wymienne, a Google traktuje oba jako przekierowania stałe.

Skąd się wzięły 303, 307 i 308

Nakładanie się kodów 301/302 i 303/307/308 to historyczny przypadek.

Specyfikacja HTTP/1.0 (RFC 1945, 1996) zdefiniowała kody 301 Moved Permanently i 302 Moved Temporarily. Stwierdzała, że agent użytkownika nie może automatycznie przekierować żądania POST bez potwierdzenia ze strony użytkownika, i już wtedy odnotowywała, że niektóre agenty użytkownika błędnie zamieniają POST na GET. Przeglądarki nie przestały tego robić.

W HTTP/1.1 pojawiły się dwa kody, które nie pozostawiają miejsca na interpretację. W RFC 2068 (1997) wprowadzono 303 See Other, który zawsze zmienia metodę na GET. W RFC 2616 (1999) dodano 307 Temporary Redirect, który zawsze zachowuje metodę, zmieniono nazwę kodu 302 na Found i otwarcie przyznano, że większość agentów użytkownika traktuje 302 jak 303.

Nadal brakowało jednak przekierowania stałego, które zachowuje metodę. Lukę tę piętnaście lat później wypełnił kod 308. W międzyczasie specyfikacja pogodziła się z rzeczywistością: od RFC 7231 (2014) agent użytkownika może zmienić POST na GET po 301 lub 302.

Dziś przeglądarki stosują następujące reguły ze specyfikacji Fetch Standard:

Kod Pierwotna metoda Przeglądarki wysyłają żądanie pod nowy adres URL metodą
301, 302 POST GET, bez treści
301, 302 Każda inna metoda Ta sama metoda
303 Każda metoda oprócz GET i HEAD GET, bez treści
303 GET lub HEAD Ta sama metoda
307, 308 Każda metoda Ta sama metoda i treść

Po 301 lub 302 zmieniane jest tylko żądanie POST; PUT i DELETE zachowują swoją metodę. Poleganie na tym jest ryzykowne, bo biblioteki HTTP działają różnie, dlatego zawsze, gdy metoda ma znaczenie, używaj 307 lub 308.

Przekierowania bez kodu 3xx

Meta refresh

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

Sama strona jest dostarczana z kodem 200 OK, a przeglądarka wykonuje przekierowanie z tagu po jej wczytaniu. Liczba oznacza opóźnienie w sekundach. Google interpretuje natychmiastowy meta refresh jako przekierowanie stałe, a opóźniony – jako przekierowanie tymczasowe.

Odświeżanie z opóźnieniem szkodzi też dostępności. WCAG akceptuje natychmiastowy meta refresh jako przekierowanie po stronie klienta (technika H76), ale opóźniony uznaje za błąd (F40), bo strona zmienia się po upływie czasu, nad którym użytkownik nie ma kontroli.

Przekierowania JavaScript

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

location.replace() nie zostawia przekierowującej strony w historii sesji, więc przycisk Wstecz nie wrzuca użytkownika ponownie w przekierowanie. Przypisanie wartości do location.href ją tam zostawia.

Google uruchamia JavaScript dopiero podczas renderowania strony, które następuje po jej skanowaniu i może się nie powieść. Dokumentacja Google zaleca przekierowania JavaScript tylko wtedy, gdy nie da się zastosować przekierowania po stronie serwera ani meta refresh. Wiele innych botów i narzędzi w ogóle nie uruchamia JavaScriptu.

Nagłówek Refresh

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

Nagłówek Refresh nie jest zdefiniowany w żadnym RFC dotyczącym HTTP. Przeglądarki obsługują go od dziesięcioleci, a HTML Standard traktuje go jako odpowiednik tagu meta refresh. Wiele klientów i narzędzi HTTP, w tym curl, go ignoruje, dlatego lepiej używać prawdziwego przekierowania 3xx.

Jak poprawnie skonfigurować przekierowania

nginx

nginx
# Every http:// request, on either host, goes to the canonical HTTPS URL in one hop
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 and ssl_certificate_key as above

    # One moved page
    location = /old-page {
        return 301 /new-page;
    }

    # A whole section, keeping the rest of the path
    location /blog/ {
        rewrite ^/blog/(.*)$ /articles/$1 permanent;
    }
}

$request_uri zawiera pierwotną ścieżkę i ciąg zapytania, więc nic nie ginie. rewrite ... permanent wysyła 301, a rewrite ... redirect – 302; w przypadku 307 lub 308 użyj return z tym kodem.

Apache (.htaccess)

Apache
# mod_alias: one page, and a whole folder (the rest of the path is appended)
Redirect 301 /old-page https://example.com/new-page
Redirect 301 /blog/ https://example.com/articles/

# mod_alias with a regular expression
RedirectMatch 301 ^/products/([0-9]+)\.html$ https://example.com/p/$1

# mod_rewrite: send every http:// and www request to https://example.com in one hop
RewriteEngine On
RewriteCond %{HTTPS} off [OR]
RewriteCond %{HTTP_HOST} ^www\. [NC]
RewriteRule ^ https://example.com%{REQUEST_URI} [R=301,L]

Redirect bez kodu odpowiedzi wysyła 302, więc zawsze podawaj kod. Za load balancerem lub CDN, które łączą się z Apache przez zwykłe HTTP, %{HTTPS} ma zawsze wartość off, a ostatnia reguła tworzy pętlę; zamiast tego sprawdzaj %{HTTP:X-Forwarded-Proto}. Moduły mod_alias i mod_rewrite działają niezależnie, a kolejność wierszy w pliku nie decyduje o tym, który z nich zadziała pierwszy. Adres URL, który pasuje do reguł obu modułów, jak tutaj http://www.example.com/old-page, może wymagać dwóch przeskoków; adresy, które muszą przekierowywać w jednym przeskoku, obsługuj regułami tylko jednego modułu.

Cloudflare

Cloudflare może przekierowywać już na brzegu sieci, zanim żądanie dotrze do Twojego serwera:

  • Single Redirects (sekcja Redirect Rules w panelu) dopasowują żądania za pomocą wyrażenia filtrującego i kierują je na statyczny lub dynamiczny adres docelowy z kodem 301, 302, 303, 307 lub 308. Typowa reguła przekierowująca z www na domenę główną ma takie ustawienia:

    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 obsługują długie listy adresów źródłowych i docelowych, np. mapę migracji przesłaną jako plik CSV. Każdy wpis ma własny kod odpowiedzi i opcje, takie jak zachowanie ciągu zapytania, a listę aktywuje reguła Bulk Redirect Rule.

  • Workers pozwalają tworzyć przekierowania w kodzie za pomocą Response.redirect():

JavaScript
export default {
  async fetch(request) {
    const url = new URL(request.url);
    if (url.pathname === '/old-page') {
      url.pathname = '/new-page'; // the query string is kept
      return Response.redirect(url.toString(), 301);
    }
    return fetch(request);
  },
};

Response.redirect() domyślnie używa 302 i akceptuje tylko 301, 302, 303, 307 i 308. Każdy inny kod powoduje zgłoszenie wyjątku RangeError.

Node.js i Express

JavaScript
import express from 'express';

const app = express();
app.set('trust proxy', 1); // when running behind a load balancer or CDN

// HTTPS and the canonical host in one hop
app.use((req, res, next) => {
  if (req.secure && req.hostname === 'example.com') return next();
  res.redirect(301, `https://example.com${req.originalUrl}`);
});

// One moved page
app.get('/old-page', (req, res) => {
  res.redirect(301, '/new-page');
});

res.redirect(url) bez kodu odpowiedzi wysyła 302. Express 5 obsługuje wyłącznie kolejność argumentów res.redirect(status, url). W czystym Node.js ustaw kod i nagłówek samodzielnie:

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

PHP

PHP
<?php
// Must run before any output is sent.
header('Location: https://example.com/new-page', true, 301);
exit;

Sam nagłówek Location sprawia, że PHP wysyła 302 (chyba że wcześniej ustawiono już kod 201 lub 3xx), więc podaj kod jako trzeci argument i wywołaj exit, aby nic więcej się nie wykonało. W WordPressie wp_redirect() i wp_safe_redirect() również domyślnie używają 302; jako drugi argument przekaż 301.

Jak wybrać właściwy kod przekierowania

  • Strona została przeniesiona na stałe i jest zwykłą stroną ładowaną metodą GET: użyj 301. Równie dobrze sprawdzi się 308.
  • Przeniesienie jest stałe, a klienci mogą wysyłać POST, PUT lub DELETE, jak w przypadku API, akcji formularzy i webhooków: użyj 308.
  • Przekierowanie jest tymczasowe i liczy się tylko GET: użyj 302.
  • Przekierowanie jest tymczasowe, a metoda musi zostać zachowana: użyj 307.
  • Formularz lub inne żądanie POST zostało przetworzone, a użytkownik powinien zobaczyć stronę z wynikiem: użyj 303.
  • Z HTTP na HTTPS: użyj 301 lub 308 i wysyłaj z witryny HTTPS nagłówek Strict-Transport-Security, aby przeglądarki następnym razem pomijały niezabezpieczone żądanie.
  • Treść zniknęła i nie ma odpowiednika: nie przekierowuj. Zwróć 404 lub 410. Przekierowanie na niepowiązaną stronę, np. stronę główną, Google może potraktować jako błąd soft 404.
  • 300, 305 i 306: nie używaj ich. Kod 304 generuje Twój serwer lub pamięć podręczna w odpowiedzi na żądania warunkowe i nigdy nie jest kodem, który wybiera się do przekierowania.

Pułapka pamięci podręcznej przy 301 i 308

Ponieważ 301 i 308 mogą być heurystycznie zapisywane w pamięci podręcznej, przeglądarki mogą je przechowywać i przestać pytać serwer. Przeglądarki oparte na Chromium zachowują 301 lub 308 bez nagłówków sterujących buforowaniem, dopóki wpis nie zostanie usunięty albo użytkownik nie wyczyści pamięci podręcznej. Błędne przekierowanie stałe może więc kierować wcześniejszych odwiedzających w złe miejsce jeszcze długo po naprawieniu serwera, a nie da się go niezawodnie wycofać z ich przeglądarek.

Jak się zabezpieczyć:

  • Testuj nowe reguły najpierw z 302 lub 307, a na 301 lub 308 przełącz się dopiero wtedy, gdy adres docelowy jest potwierdzony.
  • Możesz też na czas wdrażania ograniczyć, jak długo przekierowanie stałe może być przechowywane w pamięci podręcznej:
nginx
location = /old-page {
    add_header Cache-Control "max-age=3600";
    return 301 https://example.com/new-page;
}
  • Podczas testów używaj okna prywatnego, DevTools z włączoną opcją Disable cache albo narzędzia działającego po stronie serwera, które nigdy nie korzysta z pamięci podręcznej przeglądarki.
  • Po zmianie przekierowania wyczyść pamięć podręczną CDN i serwerów proxy, bo one również mogą przechowywać przekierowania.
  • Nie próbuj cofać zapamiętanego przekierowania A → B, przekierowując B z powrotem na A. Odwiedzający, których przeglądarki zapisały pierwsze przekierowanie, mogą wpaść w pętlę.

Testowanie przekierowań

Niezależnie od platformy sprawdź wynik z zewnątrz. Sprawdzanie przekierowań 301 potwierdza, że każdy przeskok przeniesienia na stałe to 301 lub 308, Sprawdzanie łańcucha przekierowań pokazuje każdy przeskok z jego nagłówkiem Location i pozostałymi nagłówkami, a Sprawdzanie przekierowania na HTTPS testuje naraz wszystkie cztery warianty protokołu i hosta. Sprawdzanie meta refresh znajduje przekierowania po stronie klienta, które umykają narzędziom czytającym tylko nagłówki, a Pomiar opóźnień przekierowań mierzy, ile czasu kosztuje każdy przeskok.

W terminalu polecenie curl -sIL https://example.com/old-page wyświetli nagłówki każdego przeskoku. Wiersz poleceń, DevTools i typowe problemy z przekierowaniami szczegółowo omawiamy w poradniku Jak sprawdzić przekierowania.