На первый взгляд редирект устроен просто: сервер отвечает кодом ответа и заголовком Location, а клиент переходит по указанному адресу. Но от этих трех цифр зависит, могут ли браузеры кэшировать редирект, останется ли POST-запрос POST-запросом и какой URL, скорее всего, проиндексируют поисковые системы.

В этой статье разберем класс 3xx в том виде, в каком его определяет RFC 9110, историю, из-за которой коды частично дублируют друг друга, редиректы без кода ответа и настройку нужного кода на популярных серверах.

Что означает класс 3xx

Согласно RFC 9110, действующей спецификации семантики HTTP, код ответа 3xx означает, что «для выполнения запроса клиенту (user agent) необходимо предпринять дополнительные действия». Раздел 15.4 делит эти коды на четыре вида редиректов:

  • Ресурс доступен по другому URI, указанному в Location: 301, 302, 307 и 308.
  • Клиент может выбрать одно из нескольких представлений ресурса: 300.
  • Косвенный ответ на запрос дает другой ресурс: 303.
  • Клиент может повторно использовать копию, которая уже есть у него в кэше: 304.

На практике автоматическими редиректами работают только 301, 302, 303, 307 и 308. Стандарт Fetch, который реализуют браузеры, называет «статусами редиректа» (redirect statuses) именно эти пять кодов. Ответ 300 браузер, как правило, показывает как обычную страницу, а 304 сообщает клиенту, что нужно взять копию из кэша.

Значение Location может быть относительной ссылкой, например /new-page или ../shop/. Клиент достраивает ее от только что запрошенного URL (RFC 9110, раздел 10.2.2). Если в Location нет фрагмента (части после #), сохраняется исходный: ссылка на /guide#install, которая перенаправляет на /docs/guide, приведет на /docs/guide#install.

Для пяти настоящих редиректов выбор кода зависит от ответа на два вопроса:

  1. Переезд постоянный или временный? Постоянные редиректы могут кэшироваться и сигнализируют поисковым системам, что индексировать нужно новый URL.
  2. Нужно ли сохранить метод запроса? Это важно, когда исходный запрос — POST, PUT или DELETE, а не обычный GET.

Коды редиректов: сводная таблица

Код Название Постоянный Метод сохраняется Кэшируется по умолчанию Типичное применение Google Поиск
300 Multiple Choices – – Да Выбор между альтернативными представлениями (редко) –
301 Moved Permanently Да Нет: POST может стать GET Да Перенесенные страницы и сайты, переход на HTTPS, выбор канонического хоста Постоянный (сильный сигнал)
302 Found Нет Нет: POST может стать GET Нет Краткосрочные редиректы, требование входа, A/B-тесты Временный (слабый сигнал)
303 See Other Нет Нет: становится GET Нет Страница результата после отправки формы Временный
304 Not Modified Не редирект – – Ответ на условный запрос Контент не изменился
305 Use Proxy Устарел – – Не использовать –
306 (Unused) Зарезервирован – – Не использовать –
307 Temporary Redirect Нет Да Нет Временные редиректы с любым методом Временный
308 Permanent Redirect Да Да Да Постоянный переезд API, форм и любых URL Постоянный (сильный сигнал)

«Кэшируется по умолчанию» означает, что ответ с этим кодом эвристически кэшируемый согласно RFC 9110, раздел 15.1: кэш может повторно использовать такой ответ, не сверяясь с сервером, даже если в нем нет явных сведений о сроке актуальности. При этом любой ответ можно сделать кэшируемым или некэшируемым с помощью Cache-Control. Столбец «Google Поиск» обобщает документацию Google о редиректах и HTTP-кодах ответа.

Все коды по порядку

300 Multiple Choices

У ресурса есть несколько представлений, например на разных языках или в разных форматах файлов, и клиент может выбрать одно из них. Варианты обычно перечисляются в теле ответа, причем стандартного формата для этого нет. Предпочтительный вариант можно указать в Location, но рассчитывать на то, что браузер по нему перейдет, нельзя. На практике код 300 почти не используется: серверное согласование контента или обычная страница со ссылками работают лучше.

301 Moved Permanently

Целевому ресурсу «назначен новый постоянный URI, и во всех будущих ссылках на этот ресурс следует использовать один из приведенных URI». Это стандартный код для перенесенных страниц, смены домена, перехода с HTTP на HTTPS и редиректов на канонический хост.

Важны две детали. Ответ 301 эвристически кэшируемый, поэтому браузеры могут его запомнить и перестать обращаться к серверу (см. раздел о кэшировании ниже). Кроме того, по историческим причинам клиент может заменить POST на GET, следуя редиректу 301, и браузеры так и делают. Для страниц, которые загружаются методом GET, это неважно; для обработчика форм или API используйте 308.

302 Found

Целевой ресурс «временно находится по другому URI», поэтому для будущих запросов клиентам следует и дальше использовать исходный URL. По умолчанию 302 не кэшируется. Используйте его в краткосрочных ситуациях: страница распродажи, которая на неделю заменяет категорию, требование входа, редирект по геолокации или A/B-тест.

Как и 301, код 302 допускает замену POST на GET. Многие фреймворки отправляют 302, если код не указан, — именно поэтому постоянные переезды так часто случайно оказываются 302 редиректами.

303 See Other

Код 303 отправляет клиента к другому ресурсу, который служит косвенным ответом на исходный запрос, и клиент получает этот ресурс методом GET (или HEAD). Код предназначен для шаблона Post/Redirect/Get: обработав отправленную форму, сервер отвечает кодом 303 с URL страницы подтверждения или результата. Если пользователь обновит эту страницу, повторится безобидный GET, а форма не отправится заново.

По умолчанию 303 не кэшируется и не утверждает, что исходный URL переехал. Google обрабатывает его так же, как 302.

304 Not Modified (не редирект)

Несмотря на номер из класса 3xx, код 304 никуда клиента не перенаправляет. Это ответ на условный запрос GET или HEAD — запрос с заголовком If-None-Match со значением ETag или с заголовком If-Modified-Since с датой. Если ресурс не изменился, сервер отвечает 304 без тела, и клиент использует копию из своего кэша.

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

Сервисы проверки редиректов отправляют безусловные запросы, поэтому обычно никогда не получают 304. Чтобы проверить этот код в TrueURL, запустите обычную проверку, скопируйте значение ETag из заголовков ответа и отправьте его в заголовке If-None-Match в разделе Пользовательские заголовки запроса. Для Googlebot ответ 304 означает, что контент не изменился с момента последнего сканирования.

305 Use Proxy и 306 (Unused)

Код 305 предписывал клиенту повторить запрос через прокси, указанный в Location. Позволять ответу сервера менять настройки прокси у клиента небезопасно, поэтому в RFC 7231 код объявили устаревшим, и современные браузеры на него не реагируют.

Код 306 был определен в одной из прежних версий спецификации (его часто называют Switch Proxy), а теперь зарезервирован. Ни одному из этих кодов не место в современном приложении: если вы встретили такой код, что-то настроено неправильно.

307 Temporary Redirect

Код 307 означает то же, что и 302, с одним дополнением: клиент не должен менять метод запроса. POST повторно отправляется на новый URL как POST и с тем же телом. Поэтому 307 — правильный временный редирект для API и обработчиков форм.

Код 307 также встречается в Chrome DevTools как 307 Internal Redirect с http:// на https://. Сервер его не отправлял: браузер сам перевел запрос на HTTPS, обычно из-за сохраненной для сайта политики HSTS.

308 Permanent Redirect

Код 308 — аналог 301 с сохранением метода: редирект постоянный, эвристически кэшируемый, и клиент не должен менять метод. Впервые этот код был опубликован в 2014 году в экспериментальном RFC 7238, стал стандартом с выходом RFC 7538 в 2015 году и сейчас входит в RFC 9110.

Используйте 308 для постоянного переноса эндпоинтов, которые принимают запросы POST, PUT или DELETE. Для обычных страниц 301 и 308 взаимозаменяемы, и Google считает оба кода постоянными редиректами.

Зачем нужны 303, 307 и 308

То, что 301/302 и 303/307/308 частично дублируют друг друга, — историческая случайность.

В HTTP/1.0 (RFC 1945, 1996 год) были определены коды 301 Moved Permanently и 302 Moved Temporarily. Спецификация запрещала клиенту автоматически перенаправлять POST без подтверждения пользователя и уже тогда отмечала, что некоторые клиенты ошибочно превращают POST в GET. Браузеры продолжили так делать.

В HTTP/1.1 появились два кода, которые не оставляют места для толкований. RFC 2068 (1997) ввел 303 See Other, который всегда переключает на GET. RFC 2616 (1999) добавил 307 Temporary Redirect, который всегда сохраняет метод, переименовал 302 в Found и прямо признал, что большинство клиентов обрабатывают 302 как 303.

Постоянного редиректа с сохранением метода по-прежнему не было. Этот пробел закрыл код 308 — спустя пятнадцать лет. Тем временем спецификация смирилась с реальностью: начиная с RFC 7231 (2014), клиент может заменить POST на GET после 301 или 302.

Сегодня браузеры применяют следующие правила из стандарта Fetch:

Код ответа Исходный метод Как браузеры запрашивают новый URL
301, 302 POST GET, без тела
301, 302 Любой другой метод Тем же методом
303 Любой метод, кроме GET и HEAD GET, без тела
303 GET или HEAD Тем же методом
307, 308 Любой метод Тем же методом и с тем же телом

После 301 или 302 меняется только POST; PUT и DELETE сохраняют свой метод. Но полагаться на это ненадежно, потому что HTTP-библиотеки ведут себя по-разному, так что, если метод важен, используйте 307 или 308.

Редиректы без кода 3xx

Meta Refresh

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

Сама страница отдается с кодом 200 OK, а браузер выполняет редирект из тега уже после ее загрузки. Число — это задержка в секундах. Google считает мгновенный meta refresh постоянным редиректом, а meta refresh с задержкой — временным.

Кроме того, обновление по таймеру вредит доступности. WCAG допускает мгновенный meta refresh как клиентский редирект (техника H76), но считает обновление с задержкой ошибкой (F40), потому что страница меняется по таймеру, которым пользователь не может управлять.

JavaScript-редиректы

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

location.replace() не оставляет перенаправляющую страницу в истории сеанса, поэтому кнопка «Назад» не отправляет пользователя снова на редирект. Присваивание location.href, наоборот, оставляет ее в истории.

Google выполняет JavaScript только при рендеринге страницы, который происходит после сканирования и может завершиться неудачей. В своей документации Google советует использовать JavaScript-редиректы, только если серверные редиректы и meta refresh невозможны. Многие другие боты и инструменты вообще не выполняют JavaScript.

Заголовок Refresh

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

Заголовок Refresh не определен ни в одном RFC по HTTP. Браузеры поддерживают его уже несколько десятилетий, а стандарт HTML считает его равнозначным тегу meta refresh. Многие HTTP-клиенты и инструменты, в том числе curl, его игнорируют, поэтому лучше использовать настоящий редирект 3xx.

Как правильно настроить редиректы

nginx

nginx
# Любой запрос по http:// к любому из хостов попадает на канонический HTTPS-адрес за один переход
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 и ssl_certificate_key — как выше

    # Одна перенесенная страница
    location = /old-page {
        return 301 /new-page;
    }

    # Целый раздел с сохранением остальной части пути
    location /blog/ {
        rewrite ^/blog/(.*)$ /articles/$1 permanent;
    }
}

$request_uri содержит исходный путь и строку запроса, поэтому ничего не теряется. rewrite ... permanent отправляет 301, а rewrite ... redirect — 302; для 307 или 308 используйте return с нужным кодом.

Apache (.htaccess)

Apache
# mod_alias: одна страница и целая папка (остаток пути добавляется к новому адресу)
Redirect 301 /old-page https://example.com/new-page
Redirect 301 /blog/ https://example.com/articles/

# mod_alias с регулярным выражением
RedirectMatch 301 ^/products/([0-9]+)\.html$ https://example.com/p/$1

# mod_rewrite: все запросы по http:// и к www — на https://example.com за один переход
RewriteEngine On
RewriteCond %{HTTPS} off [OR]
RewriteCond %{HTTP_HOST} ^www\. [NC]
RewriteRule ^ https://example.com%{REQUEST_URI} [R=301,L]

Redirect без кода ответа отправляет 302, поэтому всегда указывайте код. Если Apache стоит за балансировщиком нагрузки или CDN, которые подключаются к нему по обычному HTTP, %{HTTPS} всегда имеет значение off и последнее правило зацикливается; в этом случае проверяйте %{HTTP:X-Forwarded-Proto}. mod_alias и mod_rewrite работают независимо друг от друга, и порядок строк в файле не определяет, какой модуль сработает первым. URL, который подпадает под правила обоих модулей, как http://www.example.com/old-page в этом примере, может потребовать двух переходов; если URL должен перенаправляться за один переход, держите все его правила в одном модуле.

Cloudflare

Cloudflare умеет выполнять редиректы на edge-серверах, еще до того, как запрос дойдет до вашего сервера:

  • Single Redirects (раздел Redirect Rules в панели управления) отбирают запросы по выражению-фильтру и отправляют их на статический или динамический адрес с кодом 301, 302, 303, 307 или 308. Типичное правило для редиректа с www на основной домен (apex) выглядит так:

    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 работают с длинными списками исходных и целевых URL, например с картой редиректов для переезда, загруженной в формате CSV. У каждой записи свой код ответа и параметры, например сохранение строки запроса, а работать список начинает после того, как его подключит правило Bulk Redirect Rule.

  • Workers позволяют формировать редиректы в коде с помощью Response.redirect():

JavaScript
export default {
  async fetch(request) {
    const url = new URL(request.url);
    if (url.pathname === '/old-page') {
      url.pathname = '/new-page'; // строка запроса сохраняется
      return Response.redirect(url.toString(), 301);
    }
    return fetch(request);
  },
};

По умолчанию Response.redirect() использует 302 и принимает только 301, 302, 303, 307 и 308. Любой другой код вызывает ошибку RangeError.

Node.js и Express

JavaScript
import express from 'express';

const app = express();
app.set('trust proxy', 1); // если приложение работает за балансировщиком нагрузки или CDN

// HTTPS и канонический хост за один переход
app.use((req, res, next) => {
  if (req.secure && req.hostname === 'example.com') return next();
  res.redirect(301, `https://example.com${req.originalUrl}`);
});

// Одна перенесенная страница
app.get('/old-page', (req, res) => {
  res.redirect(301, '/new-page');
});

res.redirect(url) без кода ответа отправляет 302. Express 5 поддерживает только порядок аргументов res.redirect(status, url). В чистом Node.js код ответа и заголовок нужно задать самостоятельно:

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

PHP

PHP
<?php
// Вызывайте до того, как начнется вывод.
header('Location: https://example.com/new-page', true, 301);
exit;

Если задать только заголовок Location, PHP отправит 302 (если код 201 или 3xx не был установлен раньше), поэтому передавайте код третьим аргументом и вызывайте exit, чтобы дальше ничего не выполнялось. В WordPress функции wp_redirect() и wp_safe_redirect() тоже по умолчанию используют 302; передайте 301 вторым аргументом.

Как выбрать код редиректа

  • Страница переехала навсегда, и это обычная страница, которая загружается методом GET: используйте 301. Код 308 подойдет не хуже.
  • Переезд постоянный, и клиенты могут отправлять POST, PUT или DELETE, как в случае с API, обработчиками форм и вебхуками: используйте 308.
  • Редирект временный, и важен только GET: используйте 302.
  • Редирект временный, и метод нужно сохранить: используйте 307.
  • Форма или другой POST-запрос обработаны, и пользователь должен увидеть страницу результата: используйте 303.
  • С HTTP на HTTPS: используйте 301 или 308 и отправляйте с HTTPS-версии сайта заголовок Strict-Transport-Security, чтобы в следующий раз браузеры пропускали небезопасный запрос.
  • Контент удален, и замены у него нет: не ставьте редирект. Возвращайте 404 или 410. Редирект на страницу, не связанную с исходной, например на главную, Google может посчитать ошибкой soft 404.
  • 300, 305 и 306: не используйте их. Код 304 формирует ваш сервер или кэш в ответ на условные запросы; как код редиректа его не выбирают.

Ловушка кэширования 301 и 308

301 и 308 эвристически кэшируемые, поэтому браузеры могут сохранить их и перестать обращаться к серверу. Браузеры на базе Chromium хранят 301 или 308 без заголовков кэширования до тех пор, пока запись не будет вытеснена из кэша или пользователь не очистит кэш. Ошибочный постоянный редирект может еще долго отправлять бывших посетителей не туда, даже после того как вы исправите сервер, и надежно отозвать его из их браузеров нельзя.

Как обезопасить себя:

  • Сначала проверяйте новые правила с 302 или 307 и переходите на 301 или 308, когда адрес назначения окончательно подтвержден.
  • Или ограничьте время кэширования постоянного редиректа на период внедрения:
nginx
location = /old-page {
    add_header Cache-Control "max-age=3600";
    return 301 https://example.com/new-page;
}
  • При тестировании используйте окно в режиме инкогнито, DevTools с включенным Disable cache или серверный сервис проверки, который никогда не использует кэш браузера.
  • После изменения редиректа очищайте кэш CDN и прокси: они тоже могут хранить редиректы.
  • Не пытайтесь отменить закэшированный редирект A → B обратным редиректом с B на A. Посетители, у которых первый редирект сохранился в кэше браузера, могут попасть в цикл.

Проверка редиректов

На какой бы платформе вы ни работали, проверяйте результат снаружи. Инструмент «Проверка 301 редиректа» подтвердит, что каждый переход постоянного переезда — это 301 или 308, «Проверка цепочки редиректов» покажет каждый переход с его Location и заголовками, а «Проверка редиректа на HTTPS» проверит сразу все четыре варианта протокола и хоста. Инструмент «Проверка meta refresh» находит клиентские редиректы, которые пропускают инструменты, читающие только заголовки, а «Проверка скорости редиректов» измеряет, сколько стоит каждый переход.

В терминале команда curl -sIL https://example.com/old-page выведет заголовки каждого перехода. Командная строка, DevTools и типичные проблемы с редиректами подробно разобраны в нашем руководстве о том, как проверить редиректы URL.