Бич F5. Как бороться с повторной отправкой данных

14.06.2007

Когда человек заполняет форму и нажимает на кнопку «Отправить», может случиться всякое. Например, серверная часть может нормально отработать, а ответ клиенту не дойдет. Что мы (пользователи) тогда делаем? Ясное дело, нажимаем F5. Запрос отправляется еще разок с теми же данными. Серверная часть снова отрабатывает и на это раз возвращает ответ — страницу с продублированным комментарием (новостью, товаром, постом на форуме). Сталкивались с этим явлением?

Как же распространенную проблему решить? У меня четыре варианта.

1. Перед записью в базу данных можно делать запрос на предмет наличия в таблице отправляемого текста. Лишний не очень быстрый запрос. Нужно писать код проверки.

2. Можно установить для пользователя ограничение по времени, скажем, одна запись в базу в течение 30 секунд. Всё равно нужно писать код.

3. Есть вариант с редиректом: сервер возвращает легкую страничку, с которой пользователь мгновенно куда-нибудь перенаправляется. Недостаток — если легкая страничка не дойдет, то дублирования не избежать.

4. Делаем UNIQUE-индекс в таблице. Можно по нескольким полям. Смотрим, если MySQL возвращает код ошибки 1062, значит, данные уже записались. На данный момент это мой выбор. Я правда не уверен, что использую этот тип индекса по назначению, но каких-то недостатков не замечал.

Кстати, по неизвестным причинам большинство популярных CMS не решают проблему повторной записи данных. Wordpress получает плюсик.

Утреннее дополнение

Вариант с UNIQUE, как оказалось, отягощается парой недостатков.

Комментарии

Владимир Михайличенко, 14.06.2007 01:06

Есть еще пятый способ.

С помощью Ajax очень просто и удобно решать эту проблему. Заполнил поля, нажал на кнопку, если случился глюк и клиент ответа не получил, то спокойно жмет F5 и видит что его сообщение не добавилось и преспокойно повторяет опрерацию.

Серверная часть все равно два раза с темиже данными не вызывается.

Дмитрий Сергеев, 14.06.2007 10:58

Не хочется делать элементарную функциональность через Ajax, поскольку все-таки есть еще старые браузеры и люди с отключенным JavaScript.

А вариант вообще-то заманчивый.

kurapov.name, 14.06.2007 01:17

1. Есть не только INSERT, но и REPLACE запрос..
2. Можно ограничение по времени ставить не на работу с базой, а на субмит этой формы. Пишем в сессию что форма с данным именем была отослана тогда то..
3. Редирект проще делать через header('Location : ') - кода ещё меньше.

Дмитрий Сергеев, 14.06.2007 11:04

Спасибо за REPLACE, не знал. Он как раз для UNIQUE-индексов, и ошибка не возвращается. То, что надо.

Второй вариант плох необходимостью создавать сессию и что-то в ней сохранять. Лишняя возня с кодом.

Да редирект через заголовки это не решает проблему "потерявшегося ответа".

kurapov.name, 14.06.2007 01:19

п.с. насчёт отправки форм аяксом.. оно то работает, только если нет input type=file

Владимир Михайличенко, 14.06.2007 01:41

Если есть загрузка файлов, то можно сделать при помощи Ajax, но уже с использованием iframe

netklon, 14.06.2007 09:28

Есть такие инструменты как РНР-сессии и куки. Хранить уникальный айди формы проще именно с в них.

ScareCrow, 14.06.2007 10:47

>Делаем UNIQUE-индекс в таблице
чел открывает транзакцию, пытается что то вставить, получает нарушение уникальности, откатывает транзакцию и говорит что нифига братан... я могу затормозить sql сервак проще

Дмитрий Сергеев, 14.06.2007 11:07

Ну да, если производительность критична, то можно и написать немного кода, оберегающего от лишних обращений к БД. Понимаю недостаток своего способа. Спасибо.

4m@t!c, 14.06.2007 11:07

Чем больше индексов, тем больше времени на вставку. Я так понимаю уникальный индекс будет и по полю текст. А это затраты еще бОльших ресурсов. Думаю, что правильный вариант - это уникальный идентификатор заполненной и переданной формы.

Дмитрий Сергеев, 14.06.2007 11:11

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

Получаем два весомых минуса UNIQUE:
* затраты на поддержку индекса в БД,
* лишние обращения к БД в процессе работы приложения.

Пожалуй, я свои взгляды все-таки пересмотрю.

vitalaw, 19.06.2007 17:54

Можно помимо текста в базе хранить хеш этого текста и соответственно повторяемость сообщения искать уэе по его хешу. ИМХО это будет быстрее, чем индекс по текту...

Дмитрий Сергеев, 19.06.2007 21:04

Спасибо за отличную подсказку.

4m@t!c, 14.06.2007 11:15

Еще отправку делаю не по нажатию на клавишу F5, а судорожно кликая по кнопке "Отправить". Я у себя в одной форме вешал на событие onsubmit() функцию, которая делает кнопку отправки формы неактивной и перехватывает повторную отправку формы клавищей Enter.

Дмитрий Сергеев, 14.06.2007 11:24

Тоже должно нормально работать. Только немного сложновато. Вариант с уникальным id формы и сессиями выглядит легче и надежнее.

4m@t!c, 14.06.2007 11:51

Деол в том, что там идет поиск по большой базе данных и что бы нетерпеливый пользователь не создавал лишнюю нагрузку на сервер - сделанно именно так. подчеркну, что это не защита сервера от лишней нагрузки. Это просто маленький плюсик для сервера при нормальной, ежедневной эксплуатации.

Дмитрий Сергеев, 14.06.2007 11:55

А что будет, если первый запрос не дойдет, а кнопка на форме заблокируется?

4m@t!c, 14.06.2007 17:19

А у него вариантов - аж 1. Нажать F5. Опять же из практики: случаев с запаздалым откликом на порядки больше случаев с кликом и нулевым откликом.

lusever, 14.06.2007 11:17

Ну ребят... загнули.
Редирект делается после успешного запроса. И усе. Если редиректа нет, юзер go back и submit. Ну или f5, если догадается.

Дмитрий Сергеев, 14.06.2007 11:27

Скажем, сервер, на котором работает этот сайт, иногда на отправку формы возращает пустую страницу. Если нажимаешь F5, получаешь дубль. Редирект здесь, кажется, не спасет.

4m@t!c, 14.06.2007 11:47

Проблема в том, что отклик сервера выполняется продолжительный промежуток времени и до перенаправления страницы дело не доходит.

NightWriter, 15.06.2007 01:23

$_SERVER["REMOTE_ADDR"] заносим в базу, при отправке проверяем - лежит ли оно в базе, если да - то сколько времени прошло, сравниваем с таймаутом - больше, гут, удаляем из базы и принимаем форму. Одновременно и от флуда защита

Дмитрий Сергеев, 15.06.2007 01:53

Защита от флуда -- это хорошо, но лишних манипуляции с БД лучше избегать. Вариант с фиксацией уникального id формы в сессии как раз позволяет обойтись без лишних запросов.

Недостатком, конечно, может быть необходимость заводить сессию, и в некоторых случаях бороться с ее идентификатором, приписанным к URL.

Dead Krolik, 15.06.2007 18:40

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

Дмитрий Сергеев, 15.06.2007 19:01

Ну может это и не бич никакой :) Но я часто сталкиваюсь.

Psih, 25.06.2007 15:42

Лично я ещё не сталкивался с тем, что бы Header('Location: http://www.site.com/.....'); не отрабатывал. Просто в случае ошибки надо снова кидать на форму этим хедером и отображать ошибку, как вариант: Header('Location: http://www/site.com.....&error=errorCode');

P.S. Лично у меня ошибки передаються через сессию.

snow_wons, 02.07.2007 00:32

Можно сделать так:

<script language="JavaScript">
        function checkTopicForm(form)
        {form.submit();}
    </script>
</script>
<form method="post" onsubmit="checkForm(this)">

...
</form>

А в скрипте обработчике уже Header'ом делать редирект на нужную страницу