Перейти к содержанию

Перенаправления HTTP

Ресурс иногда переезжает, а результат изменения бывает удобнее читать по другому адресу. Оба случая могут сопровождаться Location, но не имеют одинакового смысла. Если клиент последует переходу с неправильным методом или повторит уже принятое создание, он получит совсем другую операцию, чем ожидал автор API.

В этом уроке разберём перенаправления на каталоге курсов. Результат — вы сможете выбрать статус по назначению перехода и объяснить, какие данные сохраняются. Примеры являются авторскими сообщениями, а не выполненными запросами. Реального перемещения catalog.example и старых URL ProfessorWeb сейчас нет.

Location не всегда просит перейти

В ответе 201 поле Location обозначало созданный курс или файл. В будущем ответе 202 оно будет указывать ресурс состояния задания. Ни один из этих статусов сам по себе не является HTTP-перенаправлением. Клиент может явно выполнить GET указанного адреса, если это нужно его сценарию, но не должен автоматически повторить туда исходный POST.

HTTP/1.1 201 Created
Location: /api/files/f001
Content-Type: application/json
Cache-Control: no-store

{"id":"f001","bytes":12,"mediaType":"text/plain"}

Здесь загрузка уже принята. POST по /api/files/f001 не становится её обязательным продолжением. Адрес результата и маршрут команды различаются. Это полезно проверять при чтении клиентского кода: слишком общий обработчик «если есть Location, отправить ещё раз» нарушает договор даже без статуса 3xx.

Для собственно перехода используется подходящий код из группы 3xx. Location может быть относительным, как в примерах, и разрешается относительно адреса запроса. После разрешения клиент должен понимать конечный origin и смысл метода, а не доверять внешне знакомому фрагменту строки.

Переезд читаемого адреса

Предположим, для публичного чтения существовал адрес /api/catalog/c001, а каноническим стал /api/courses/c001. В пределах одного origin можно описать постоянный переход:

HTTP/1.1 308 Permanent Redirect
Location: /api/courses/c001
Cache-Control: max-age=3600

Этот пример относится к GET старого псевдонима. 308 обозначает постоянный перенос и сохраняет метод при автоматическом переходе. Временный аналог 307 не обещает постоянного нового адреса. Выбор времени хранения перенаправления — отдельное решение: старый кеш способен продолжать отправлять клиента по адресу даже после изменения серверной настройки.

Если URL является важной страницей сайта, сохранение работоспособного старого пути часто удобнее самой смены пути. В ProfessorWeb мы ориентировались на неизменные адреса материалов. Данный урок касается механики API-перехода; план миграции индексируемых страниц, canonical и поисковой карты остаётся задачей сайта, а не выводом из одного кода 308.

Сохранить метод и тело

Пусть endpoint изменения действительно переносится на новый путь до выполнения операции. Для временного переноса применим 307:

HTTP/1.1 307 Temporary Redirect
Location: /api/editor/courses/c001
Cache-Control: no-store

При следовании такому переходу PUT остаётся PUT, а его тело не превращается в пустой GET. Смысл подтверждения версии тоже должен сохраняться: If-Match относится к тому представлению, которое договор нового пути считает текущим. Если два адреса в нашей системе являются псевдонимами одного ресурса, это нужно подтвердить общей моделью, а не сходством имени.

Это условный дополнительный маршрут, который не введён в основной контракт каталога. Мы не утверждаем, что по нему уже работает сервер. Для реального переезда пришлось бы сохранить правила права Alice, допустимого JSON, старой версии, отказов и повторяемости. Перенаправление само по себе не переносит бизнес-ограничения.

307 и 308 полезны именно тогда, когда требуется сохранить операцию. Однако способность клиента повторно передать поток тела не гарантирована: файл мог быть прочитан из невозобновляемого источника. Поэтому договор загрузки должен учитывать клиент и не предполагать бесконечно воспроизводимое тело только на основании статуса.

Переход к отдельному результату

Другой сценарий: действие уже выполнено, и сервер предлагает посмотреть его результат. Для традиционного браузерного POST подходит 303 See Other:

HTTP/1.1 303 See Other
Location: /courses/c001/
Cache-Control: no-store

В этом случае последующее получение обычно выполняется GET; для исходного HEAD используется HEAD. Страница /courses/c001/ здесь условный HTML-результат, а не замена учебного JSON API. Наш API создания по-прежнему возвращает 201 с JSON, поскольку клиенту нужна структурированная информация.

Смысл 303 — посмотреть другой ресурс, а не повторить команду. Это помогает отделить отправку формы от отображения результата. Но потеря самого ответа на POST всё равно оставляет неопределённый исход: клиент ещё не знает, было ли действие принято. Перенаправление не заменяет прикладной ключ и хранение результата операции.

Почему 301 и 302 требуют осторожности

301 обозначает постоянный переход, 302 — временный. Историческое поведение допускает, что при переходе после POST клиент изменит метод на GET. Поэтому в API нельзя выбрать 302 и одновременно требовать, чтобы каждый клиент непременно повторил POST с исходным телом. Если сохранение метода является частью договора, подходят 307 или 308. Семантика перенаправлений RFC 9110.

Для обычного GET различие методов не возникает, однако постоянство и кеширование остаются важными. Временное перенаправление авторизации не стоит случайно делать постоянным. И наоборот, постоянный переезд публичной страницы должен быть согласован с маршрутизацией и жизненным циклом нового адреса.

Не используйте 302 на страницу входа вместо 401 в нашем API. Клиент ожидает JSON или problem, а после автоматического перехода может получить успешный 200 с HTML login. Тогда факт отсутствия credentials окажется скрыт. Браузерный интерфейс может показывать собственный вход после 401, сохраняя договор API.

Credentials привязаны к границе доверия

Переход на другой origin требует отдельного решения. Нельзя вручную копировать Bearer, Cookie или CSRF token в запрос произвольному адресату только потому, что Location пришёл от знакомого сервера. Авторизация предназначалась catalog.example, а новый домен может не иметь права видеть эти значения.

Браузерный Fetch имеет собственные правила обработки перенаправлений, чувствительных заголовков и CORS. Они не означают, что самописный HTTP-клиент автоматически повторит такое безопасное поведение. HTTP redirect fetch в WHATWG Fetch.

Cookie также выбирается браузером по области нового запроса. Host-only cookie нашего каталога не становится cookie чужого host. При проверке разрешённой цели сравнивайте разобранные scheme, host и port, а не строку «catalog.example» внутри длинного URL. И ограничивайте число переходов, иначе петля от A к B и обратно может расходовать соединения бесконечно.

Перенос не создаёт повторяемость

Представим, что старый POST уже создал ресурс, но ответ потерялся. После переезда клиент послал те же данные и Idempotency-Key новому origin с независимым реестром ключей. Новый сервер не знает прошлый результат и способен создать дубликат. Сохранение метода при 308 не решает эту проблему.

Для миграции нужна общая история операций либо явный способ разрешить неопределённый результат на старой стороне. Область нашего ключа включает subject, метод и путь; изменение пути тоже требует согласования псевдонимов и реестра. Нельзя обещать прежнюю дедупликацию, оставив её состояние в отключённом процессе.

При будущем ручном опыте следует смотреть конечный URL, метод, отправленные credentials и число созданных ресурсов. Мы ничего из этого не выполняли: результат урока — различение переезда до операции, отдельного чтения после операции и переноса уже существующей истории. Следующий урок задаёт ещё одну причину не отправлять повтор немедленно: исчерпание частотного бюджета.

Примеры продолжения серии. Оглавление курса.