Редирект после POST запроса

Каждый веб-разработчик знает, что после POST сабмита формы желательно сделать редирект, чтобы предотвратить повторную отправку данных, когда пользователь захочет обновить страницу. В основном это критически необходимая операция, так как данные формы могут сохраняться в базе данных или участвовать в платёжной транзакции. И тогда данные не только продублируются, но и спишутся лишние деньги.

Но речь не о деньгах, а о правильном редиректе…

Практически все веб-приложения при редиректе POST запроса возвращают статус 302 Found. Например, в php редирект делают так: header('Location: /new/location');. Без дополнительных параметров или если отдельно не указан другой статус, функция вернёт именно 302 Found.

Теперь обратимся в официальным документам. В RFC 2616 сказано следующее:
If the 302 status code is received in response to a request other than GET or HEAD, the user agent MUST NOT automatically redirect the request unless it can be confirmed by the user, since this might change the conditions under which the request was issued.

Если статус 302 получен в ответ на запрос отличный от GET или HEAD, юзер-агент НЕ ДОЛЖЕН автоматически редиректить запрос до тех пор, пока он не будет подтверждён пользователем, так как это может нарушить условия запроса.

Там же в заметках написано, что несмотря на это, многие юзер-агенты пренебрегают этим правилом и интерпретируют 302 статус как 303. А пошло это ещё со времён HTTP/1.0, в котором 303 статуса ещё не было.

Т.е. для редиректа POST запроса нужно использовать статус 303 See Other, который специально для этого и предназначен. В php редирект будет выглядеть, например, так: header('Location: /new/location', true, 303);

В RFC в заметке к статусу 303 написано:
Many pre-HTTP/1.1 user agents do not understand the 303 status. When interoperability with such clients is a concern, the 302 status code may be used instead, since most user agents react to a 302 response as described here for 303

Многие пре-HTTP/1.1 юзер-агенты не понимают 303 статус. Если совместимость с такими клиентами важна, то вместо него можно использовать 302 статус, так как большинство таких агентов реагируют на 302 статус также как на 303.

И получается два варианта:
1. По прежнему использовать 302;
a. есть вероятность нарваться на юзер-агента, который чтит спецификацию и выдаст предупрежление.
б. так как такое поведение не стандартно, можно нарваться на вообще непредсказуемый результат.

2. Использовать 303, тогда старые клиенты не поймут, что от них хотят.

Во втором случае, можно анализировать версию протокола, запрошенную клиентом, и выдавать 302 для старых клиентов. В теле ответа писать ссылку на новый УРЛ. Тогда пользователь старого агента, сможет хотя бы кликнуть на ссылку.
+70
3 марта 2010, 21:28
103

комментарии (78)

–21
n3m0 #
Интересную конечно тему подняли. Теперь стоит думать, что и где применять
+18
le0pard #
>>есть вероятность нарваться на юзер-агента, который чтит спецификацию и выдаст предупрежление.

Пока про такой юзер-агент не слышал. Как появится — будем решать проблему :)
+1
claustrofob #
Я тоже. Но есть вероятность, что они появятся, например, среди мобильных приложений. В любом случае, лучше быть в курсе=)
+10
GarretUA #
Да никто не будет делать такое. Везде нормально редиректится, а в новом продукте не будет? Кто после этого будет его использовать? :)
+2
khizhaster #
А так же (к автору поста):
> Использовать 303, тогда старые клиенты не поймут, что от них хотят.
это какие, например?
+7
claustrofob #
Например:
Opera младше 4.0,
Internet Explorer младше 4.0

Может для кого-то критична поддержка и этих браузеров=)
+3
StrangeAttractor #
В таком случае, при всей моей романтической любви к PC-антиквариату, «следовать стандарту и использовать 303 или заботиться о совместимости со старыми клиентами и использовать 302?» кажется мне вопросом из компетенции капитана Очевидность.
+1
zhekanax #
Например, Zend_Http_Client из ZF с включенной опцией strict_redirects, которая, кстати говоря, по умолчанию выключена.
–5
youROCK #
Мне кажется лишним выносить подобную логику в PHP (или любой другой серверный язык), выдачей «правильного» кода должен заниматься веб-сервер, когда увидит заголовок Location, если это действительно может на что-то повлиять (поскольку Apache отправляет 302, вероятно, это-таки ни на что не влияет :)). Так-то!
+9
barker #
Ну прям уж) Не должен сервер это делать, вы что. Что значит «правильный код»? Это должно приложение решать — какой код возврата «правильный», это прикладное понятие, а не протокольное.
+2
w999d #
Ради такого случая почистил свою функцию редиректа

function redirect($url, $html = '', $title = 'Переадресация'){
	header($_SERVER['SERVER_PROTOCOL']." 303 See Other");
	header("Location: ".$url);
	header("Content-type: text/html; charset=UTF-8");
	$hUrl = htmlspecialchars($url);
	echo <<<PAGE
<!DOCTYPE html><title>{$title}</title>
<script type="text/javascript">function doRedirect(){location.replace({$hUrl});}</script>
<style type="text/css">p{font-family:Arial, sans-serif}</style>
<body onload="doRedirect()" bgcolor="#ffffff" text="#000000" link="#0000cc" vlink="#551a8b" alink="#ff0000">
<noscript><meta http-equiv="refresh" content="{$wait}; url='{$hUrl}'"></noscript>
<p>Подождите...</p>
<p>Если переадресация не сработала, перейдите по <a href="{$hUrl}">ссылке</a> вручную.</p>
{$html}
</body>
PAGE;
	die();
}
+1
w999d #
все-таки пропустил кое-что:
$wait = 0
НЛО прилетело и опубликовало эту надпись здесь
0
zhekanax #
Не думаю что return будет корректным. В фреймворках обычно бросается некий Redirect_Exception, который обрабатывается контроллером (корректно завершается работа). А если контроллера, как такового, нет то die() или exit именно то что нужно.
НЛО прилетело и опубликовало эту надпись здесь
0
zhekanax #
Ох, озадачили. Фреймворк это набор компонентов, с уже решенными типичными (и не очень) задачами а также облегчающие и ускоряющие разработку. Например ZF, Symfony

Или это шутка? )
+1
claustrofob #
это тролль
НЛО прилетело и опубликовало эту надпись здесь
0
zhekanax #
табличку надо было по-больше
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
+3
Setti #
это для чувства глубокого удовлетворения ;)
+1
w999d #
этот html-код практически такой же, как у гугла — у меня это сделано именно «для чувства глубокого удовлетворения ;)»
0
w999d #
а, да, вспомнил — я долго удивлялся, почему гугл умудряется пересылать меня в почту с выключенными редиректами — поэтому залез посмотреть и сделал примерно так же.
0
homm #
В Опере можно отключить редиректы и я часто это делаю для отладки.
НЛО прилетело и опубликовало эту надпись здесь
0
egorinsk #
Яваскрипт мерзкий убрали бы…
+1
kolpeex #
Тогда уж было бы правильным ввести условие для header($_SERVER['SERVER_PROTOCOL']." 303 See Other").
Чтобы при $_SERVER['SERVER_PROTOCOL']=='HTTP/1.0' возвращался 302-й код.
–5
IGlukhov #
Неужели так сложно сделать защиту от повторного постинга без редиректа?
Всё же не для этого ИМХО редиректы придуманы.
0
FuN_ViT #
да легко. например в форму внедряется скрытый ключ…
+5
w999d #
> Неужели так сложно сделать защиту от повторного постинга без редиректа?
от дублирования информации — можно, от повторной отправки формы по F5 — нет (кроме ajax).
помимо этого иногда требуется отправить клиента на совершенно другую страницу.

> Всё же не для этого ИМХО редиректы придуманы.
цитата из RFC: «This method exists primarily to allow the output of a POST-activated script to redirect the user agent to a selected resource.»
0
IGlukhov #
Да, про 303 конечно соглашусь — ваша правда.

Хотя мне как-то ближе способ с дополнительным уникальным скрытым полем — он ко всему прочему от ботов защищает.
+3
w999d #
они хорошо друг друга дополняют — иногда пользователи успевают несколько раз отправить форму (кликая по кнопке в ожидании), еще не дойдя до редиректа.
0
claustrofob #
ещё помогает задизэблить кнопку после нажатия
0
w999d #
если яваскрипт включен, конечно же
–1
simonoff #
а сейчас это такая большая редкость?
+3
kost #
Если потом вернуться кнопкой «Back», то кнопка окажется заблокированной, и придется перезагружать страницу, чтобы ее разблокировать.
НЛО прилетело и опубликовало эту надпись здесь
0
IGlukhov #
Да, вы совершенно правы — это ещё один аргумент чтобы отказаться от редиректа в пользу скрытого поля :)
0
w999d #
скрытое поле не может отправить пользователя на другую страницу, в этом смысле они бесполезны.
0
IGlukhov #
Совершенно верно. Но в топике шла речь именно о повторной отправке данных.
+2
w999d #
Речь шла о редиректе, повторная отправка была в качестве примера для вступления:
«Но речь не о деньгах, а о правильном редиректе…»
0
radist2s #
Ну, а откуда возьмется скрытое, сразу поле после того, как юзер сабмитил форму, если страница еще не успела загрузиться снова?
+1
IGlukhov #
Скрытое поле генерится *до* сабмита формы и передаётся вместе с ней.
Если сервер видит что такое значение ему уже передавали — даёт отлуп.
0
radist2s #
Я имел ввиду вот этот момент:
>иногда пользователи успевают несколько раз отправить форму (кликая по кнопке в ожидании)
То есть если по вашему, то все равно нужно генерить яваскриптом либо новое значение для хайдена, либо дисэблить кнопку.
Однако, редирект — это более кошерно.
+1
IGlukhov #
Значение генерится не на клиенте, а на сервере. Второй раз ничего генерить как раз не нужно — мы эту форму второй раз принимать не хотим.
0
radist2s #
Черт, даже стыдно, что для меня пришлось разжевывать, но буду убеждать себя, что это все из-за сложного дня, а не из-за того, что тупею.
0
zhekanax #
В том-то и дело что не хотим, и обязательно скажем об этом пользователю выведя ошибку вместо красивой странички с успешной регистрацией :)
0
sovnarkom #
оно там было с самого начала, с произвольным значением — если запросы приходят с одинаковыми значениями — нужно игнорировать все, кроме первого.
0
IGlukhov #
Второй вариант — брать не произвольные, а известные значения для этого поля, чтобы сервер знал, какую форму он сгенерил, а какую — чужой дядя.
+6
CawaSPb #
Отказываться от редиректа все-таки не надо.
С редиректом для пользователя сохраняется возможность свободно гулять по истории без предупреждений броузера «сейчас вы повторно отправите какие-то данные».

Т.е. использовать POST запросы только как setter'ы (что-то меняющие на стороне сервера, но ничего не отображающие), а GET — как getter'ы (только отображающие, но никогда не меняющие ничего на стороне сервера) — идеологически правильно.

Более того, если мы переходим по какому-то специальному урлу (линк с картинкой кнопки) и при этом что-то меняется в даных на стороне сервера, правильно и после этого сделать редирект.
0
nekt #
Согласен. crud далеко не худший способ организации веб-приложения.
+1
claustrofob #
для бота не проблема запросить форму с уникальным скрытым полем перед отправкой. это скорее защита от CSRF.
+1
IGlukhov #
Это если он не совсем тупой.
А большинство ботов (по крайней мере те, которые мне пытаются досаждать) — совсем тупые.
0
Methos #
Отправлять форму ajax-запросом, если включен js.
0
gag_fenix #
Если автор поста проведет тестирование различных браузеров в такой ситуации, мы ему будем очень благодарны. Может быть все браузеры корректно себя ведут?
0
claustrofob #
Не понял вопроса. Что значит «все браузеры корректно себя ведут»? Все браузеры, поддерживающие HTTP/1.1 понимают 303 статус. И все популярные браузеры интерпретируют 302 статус как 303.
0
Gero #
Совершенно верно, все браузеры не являющиеся музейными экспонатами, ведут себя корректно. Я думаю, это вопрос скорее академического характера.
+2
claustrofob #
скорее все браузеры ведут себя некорректно)
–1
track13 #
если есть спецификация, которая всеми выполняется как-то по другому, то наверное в спеке ошибка, а не в реализациях.
–1
Ueasley #
Есть какая практическая польза?
0
homm #
Через 10 лет ваше приложение продолжит работать правильно, в то время как у других бразуер будет спрашивать пользователя при переходе.
0
andoriyu #
заметил, что в последнем сафари все чаще приходится тыкать на кнопку «если вас не перебросило автоматически тыкните сюда»… это оно? уже началось?
–1
alice2k #
Мне кажется, что правильнее сделать так: в пост запросах на выполнение каких-то значительных действий(списание средств/ удаление данных / итд) должен присутствовать уникальный номер запроса и все последующие запросы с этим номером не должны выполнятся.
0
claustrofob #
это факт. потому что запостить повторно форму можно и умышленно.
0
P_r_i_m_a_t #
Напомнило последний эпизод The Big Bang Theory:
— Undoubtedly another snide response to my repeated letters complaining that the flags in front of the courthouse are flying in the wrong order. From left to right, it's supposed to be federal, state, and then city of Pasadena.

— I'm sorry. You sent more than one letter about that?

— It bothers me.
+1
Boiler #
Врядли кто-то сейчас использует настолько старый клиент, который не поддерживает HTTP/1.0
Большинство сайтов хостятся на виртуальных хостингах, где без HOST заголовка не попадешь на нужный сайт.
0
Boiler #
тьфу…
* который не поддерживает HTTP/1.1
+1
bondbig #
>Врядли кто-то сейчас использует настолько старый клиент, который не поддерживает HTTP/1.0
Если твои запросы в мир идут через squid, скажем, то они будут именно http/1.0. А таких ой как немало.
+2
symbix #
например, так: header('Location: /new/location', true, 303);

Например, не так. URL в Location должен быть абсолютным:

The field value consists of a single absolute URI.

Location = «Location» ":" absoluteURI

RFC 2616, п.14.30.
+1
claustrofob #
И правда:) Несмотря на то, что все браузеры поддерживают относительные URL'ы, по стандарту он должен быть абсолютным.
0
symbix #
302 тоже все браузеры поддерживают.

уж если педантствовать — то во всем! ;)

кстати, если предположить, что некий сферический браузер в вакууме реализован строго по букве стандарта, то 302 не так страшно как относительный адрес.
–1
symbix #
есть еще одна проблема, на сей раз уже практическая.

ныне совершенно типична конфигурация сервера, когда фронтендом стоит nginx, проксирующий динамику на апач. Так вот проксирование делается по http/1.0; ответ http/1.1 будет просто некорректен (и может иметь крайне негативные последствия в виде отдачи chunked transfer encoding nginx-у, сделавшему запрос по 1.0 и этого никак не ожидающему); a http/1.0 303 формально некорректен, что будет практически — надо проверять.
0
w999d #
на практике nginx понимает заголовок Host:, которого тоже нет в http/1.0
0
symbix #
И прекрасно понимает http/1.1, пока это не касается работы с бэкендом.

Кстати host в 1.0 никаким образом не запрещается, в 1.1 же он просто обязателен
+5
Snick #
Хороший пример рака мозга, причем, заразного.

Итак, в вебе миллионы серверов с миллионами приложений, которые написаны с использованием 302. Старые клиенты, которые не знали про 303 постепенно отмирают, но приложения на серверах остаются и по-прежнему используют 302 вместо 303. И будут, потому что это вошло в образование веб-программистов и впитано с исходниками примеров из книжек.

Разработчики стандарта поступили не очень мудро. Нужно было быть ближе к народу и наделить 302 именно тем смыслом, который в него вкладывают те, кто его использует. А «старый 302» переименовать в 303-й =)

И все дела =)
–1
ProtoPlex #
Не надо париться по этому поводу :) Направление уже задано, и переделывать кучу софта смысла нет
0
funca #
задумывался об этом, читая rfc. но пришел к выводу, что фактически это бесполезная хрень. все клиенты понимают 302 и автоматически редиректят, поэтому инсинуации в rfc про 303 ни кого не волнуют. :)

«проблему» стоило бы порешать лишь в одном случае — если бы браузеры действительно запрашивали потдветждение юзера при 302 редиректе. но этого нет и никогда не будет, потому, что так устроен современный интернет. :)

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.