Впервые за пять лет разработки интернет-сайтов я столкнулся с весьма неожиданной проблемой, стоившей мне многих часов поиска, нервов и волос на голове. Внезапно я обнаружил, что на новом сайте, который у меня сейчас в разработке на локалхосте, дублируются INSERT запросы к БД. Отправляю один комментарий через форму, а в базу вставляются два. Если вы не знаете, как связана эта проблема с Chrome, favicon.ico и ModRewrite, то добро пожаловать под кат.

Нет, это не в глазах двоится


Разумеется, первое что пришло в голову — где-то в скрипте «двоится» запрос к базе данных. Например, такая «школьная ошибка»:

$query = "INSERT INTO table VALUES(...)";
$result = mysqli->query($query);
if ($result = mysqli->query($query)) {
...
}

Проверяю сто тысяч раз — нет. С кодом все в порядке. Значит браузер по какой-то причине отправляет данные дважды. Генерирую простенький тестовый скрипт с инкрементом сессионной переменной и — да! Вместо увеличения на единицу браузер упорно показывает увеличение переменной на два. Пробую вместо Хрома Сафари — нет такой проблемы. Далее идут поиски некорректно работающих яваскриптов, расширений для браузера, но все безрезультатно. Поиск по интернету давал схожие советы. Многие программисты в аналогичной ситуации вводили проверку на уникальность и отсекали дублирующиеся посты. Но баг то это решение не устраняет и я решил не останавливаться и найти причину такого поведения.

Наконец, причина была найдена. И так как решение нашел с трудом, на англоязычной девелоперской ветке, хочу поделиться им здесь. Все просто — виновника два: Google Chrome (и производные от него браузеры) и mod_rewrite в Apache.

Суть проблемы


Все просто. Особенность браузеров, построенных на платформе Crome в том, что они ищут файл favicon.ico для каждого сайта. И если его нет, то они все равно будут упорно его искать. При каждом обновлении страницы, отдельным запросом к серверу. А большинство .htaccess файлов в Apache имеют в себе строчки:

RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule . /index.php

Руководствуясь этим правилом, получая запрос от браузера к несуществующему favicon.ico Apache послушно перенаправит его к файлу index.php и скрипт отработает дважды. Конечно, в большинстве случаев полноценные веб-приложения имеют проверку на уникальность и не пропускают повторяющиеся запросы. А сайты в большинстве случаев имеют файл favicon.ico. Но все же раз существует такая проблема, значит можно описать методы решения.

Варианты решения


Решение первое, самое простое: заведите на сайте favicon.ico. Chrome найдет его и успокоится.
Решение второе — немного изменить файл .htaccess на сервере. У меня блок правил mod_rewrite для всех проектов теперь будет выглядеть так:

<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
# Next line is to solve the Chrome and favicon.ico file issue.
# Without it browser sends two requests to script.
RewriteCond %{REQUEST_FILENAME} !favicon.ico
# RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>

Не поставил favicon? Получи двойной трафик от Chrome


Если вдуматься, то утверждение справедливо, ведь вместо того, чтобы отдать «статику», сервер будет запускать веб-приложение, которое в лучшем случае вернёт в браузер вместо иконки динамически сгенерированную 404 страницу. А в худшем — отработает полностью запуск главной страницы сайта. Получается, что не установив иконку на сайте (например, на одном популярном блоговом движке), разработчик вдвое увеличивает нагрузку на сервер от пользователей Google Chrome.

Вот, собственно и все. Надеюсь эта информация кому-то пригодится и сэкономит время.
Поделиться с друзьями
-->

Комментарии (59)


  1. elfiki
    07.09.2016 17:06
    +34

    Тут скорее отсутсвие обработки запросов, которые должны 404 страницу возвращать, а не отсутствие фавикона.
    Ну, т.е. странно что обращение к "/favicon.ico" и "/comment/" какой-нибудь обрабатываются одинаково


    1. AndreyRubankov
      07.09.2016 17:17
      -20

      Во-первых, есть кейс, когда пользователя с 404 лучше отправить на home page, чтобы он не ушел. Во-вторых, иногда даже 404 страница должна быть не просто статикой (редко, но бывает).

      В результате любой 404 может превратиться в нагрузку на сервер.
      Проблема для продакшена не частая — почти любой сайт имеет favicon, но тем не менее.


      1. elfiki
        07.09.2016 17:24
        +5

        Ну ок, в случае когда идет отправка на главную — то получится редирект и дополнительный показ главной страницы, но никак не добавление комментария. Ну ок, в случае когда 404 не просто статика, то все-равно это страница 404, а не добавление комментария в базу.


        1. AndreyRubankov
          07.09.2016 18:23
          -4

          Думаю, автор пример добавления в базу добавил просто для того, чтобы показать «ААА, Мы все умрем!», и судя по-всему, его за это и минусуют.

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


      1. DistortNeo
        07.09.2016 17:52
        +1

        Во-первых, есть кейс, когда пользователя с 404 лучше отправить на home page, чтобы он не ушел. Во-вторых, иногда даже 404 страница должна быть не просто статикой (редко, но бывает).

        Ну так делайте динамический переход с помощью JavaScript.


        1. AndreyRubankov
          07.09.2016 18:32
          -2

          Иногда js в браузере может быть выключен и иногда все же есть требования, чтобы сайты работали без js. Да, это не можно, не круто, но иногда бизнесу так выгоднее. А соответственно js редирект не будет работать.

          А еще есть прикол с Referer при редиректе через js (иногда это тоже важно). При серверном редиректе referer нового запроса будет первоначальная страница, а при js редиректе — текущая страница.


          1. DistortNeo
            07.09.2016 19:08

            Иногда js в браузере может быть выключен и иногда все же есть требования, чтобы сайты работали без js

            Для этого существует тэг noscript. Включены скрипты — пользователь видит пустую страницу, с которой тут же идёт переход. Выключены скрипты — пользователь получает простую статическую страницу.

            А еще есть прикол с Referer при редиректе через js (иногда это тоже важно). При серверном редиректе referer нового запроса будет первоначальная страница, а при js редиректе — текущая страница.

            Это случится, если при некорректном запросе сначала делать редирект на страницу 404, а не отдавать код сразу.


            1. AndreyRubankov
              08.09.2016 09:33
              -2

              > Для этого существует тэг noscript.
              — пользователь пришел на 404.html и ушел, это не то, что он ожидал увидеть и ничего, что привлекло бы его внимание тут нету. пользователь ушел.

              серверный редирект позволит сразу его перекинуть на хомпейдж, и там уже есть вероятность, что он задержится дольше, чем на 404.html

              А решение проблемы с /favicon очень простое — нужно правильно настроить рулы редиректа. Для всех картинок прописать 404, без редиректа, а для 404 при запросе html — прописать редирект на хомпейдж. Велосипедов придумывать не нужно, все уже давно известно.

              > Это случится, если при некорректном запросе сначала делать редирект на страницу 404, а не отдавать код сразу.

              сценарий:
              — пользователь из внешнего источника «А» тыкает на ссылку, которая ведет на отсутствующую страницу (почему отсутствует — другой вопрос).

              страницы нету — он попадает на 404.html, на которой стоит редирект на хомпейдж. перейдя на хомпейдж — у него в referer будет 404.html, а не «ресурс А»

              единственный способ сохранить referer — серверный редирект.


              1. DistortNeo
                08.09.2016 12:37
                +1

                А решение проблемы с /favicon очень простое — нужно правильно настроить рулы редиректа.

                Да, это более правильный способ.

                страницы нету — он попадает на 404.html

                А зачем делать редирект на 404.html, когда можно просто отдать код страницы?
                Тогда в Referer будет не 404.html, а адрес некорректного запроса. Если же позарез нужен именно «ресурс A», тогда можно его передавать через параметры запроса.

                Впрочем, возни с сервисами аналитики это прибавит.


                1. AndreyRubankov
                  08.09.2016 15:07

                  > А зачем делать редирект на 404.html, когда можно просто отдать код страницы?

                  Спасибо, интересный вариант, про него не подумал про него!

                  Но есть 2 «но»:
                  1. Отдав контент и далее сделав редирект — Referer у редиректа будет запрошенная страница (которой нету).
                  2. Этот вариант займет больше времени. Сначала нужно доставить контент (объем больше, чем у пустого редиректа), потом нужно будет этот контент отрендерить, и лишь потом будет выполнен редирект.


                  1. AleXP3
                    08.09.2016 15:41

                    1. Отдав контент и далее сделав редирект — Referer у редиректа будет запрошенная страница (которой нету).

                    Не будет, если сайт спроектирован и настроен правильно. И вот почему:

                    _RFC 2616_
                    14.36 Referer

                    The Referer[sic] request-header field allows the client to specify,
                    for the server's benefit, the address (URI) of the resource from
                    which the Request-URI was obtained (the «referrer», although the
                    header field is misspelled.) The Referer request-header allows a
                    server to generate lists of back-links to resources for interest,
                    logging, optimized caching, etc. It also allows obsolete or mistyped
                    links to be traced for maintenance. The Referer field MUST NOT be
                    sent if the Request-URI was obtained from a source that does not have
                    its own URI, such as input from the user keyboard
                    .


                    Случай с прямым набором с клавиатуры — частный. Но браузеры не выставляют Referer и в случаях когда напарываются на отсутствующий на сервере контент и получают в ответ от сервера код состояния 4** b редирект на другую страницу.


                    1. AndreyRubankov
                      08.09.2016 15:53

                      Кажется вы не к тому комменту ответили. Ваши слова лишь подтверждают мои.

                      Моя позиция в том, что для сохранения referer нужно делать 3xx редирект html контента.

                      В сообщениях выше по этой ветке коллеги предлагают отдать контент и уже из контента делать редирект. Отдать html контент с 404 кодом, даже если и получится, то это не правильно.

                      И если таки отдать 404.html — то при редиректе с нее в реферер будет установлен 404.html, а не ресурс, с которого пришли.


              1. Vadiok
                08.09.2016 12:53

                Все-таки лучше не сразу отправлять на главную, а показать 404 и, если уж так надо, то добавить мета-тег c [http-equiv="refresh"], чтобы пользователя перекидывало на главную через несколько секунд (работает без JS):


                <meta http-equiv="refresh" content="3; URL=http://example.com/"> 


              1. DistortNeo
                08.09.2016 13:23

                Кстати, ещё одна причина отдать именно страницу 404 — банальное следование стандартам. При некорректном запросе вы должны вернуть HTTP-ответ с кодом 4**, иначе будете вводить как браузеры, так и поисковые страницы в заблуждение.


                1. AleXP3
                  08.09.2016 15:21

                  Строго говоря: отдать 4** статус, но не обязательно страницу.


                1. AleXP3
                  08.09.2016 15:46

                  Дополнение:

                  Без статуса 404, на месте отсутствующего контента, даже, например, через «инструмент вэбмастера» Яндекса, убрать из поискового массива страницу не получится, сколько не натравливай робота. Так что играться со статусами, и выставлять их заведомо не правильно или забить на них вообще, это совершенно не богоугодное дело.


      1. AndreyRubankov
        07.09.2016 18:39

        Судя по минусам, мой коммент не поняли.

        Запись в базу — это тупость, без сомнения. Судя по всему, автор хотел нагнать паники «Мы все умрем».

        А так же бизнес кейсы бывают разные. Не все делается на фронтенде, как бы этого не хотелось многим.


      1. zapimir
        07.09.2016 18:52
        +8

        Какой Home page по 404, если речь о favicon.ico? Вы на запрос иконки собираетесь отдавать html?
        Банально у автора неправильно настроена обработка путей. Это довольно частая проблема, среди тех кто любит бездумно все запросы заворачивать на index.php.


        1. AndreyRubankov
          08.09.2016 09:14

          Я не собираюсь уже поверьте.

          Но вот есть множество проектов, которые сделаны на столько коряво, что и на /favicon отдается 404.html и даже на ошибочный рест запрос тоже 404.html словить можно.


    1. mmman
      15.09.2016 10:57

      Это обычное дело для систем типа Wordpress, где ЧПУ формируются автоматически. Вместо того, чтобы в htaccess описывать все возможные маски названий страниц, происходит перенаправление на index, а дальше тот сам в базе ищет соответствующий пост.


  1. saluev
    07.09.2016 17:20
    +26

    Мне кажется, статья несколько неполная. Например, заголовок я бы дополнил так:

    Не поставил favicon на сайте, халтурно сокнфигурировал Apache и по-дурацки организовал работу скриптов и БД — получи двойной трафик от Chrome

    А одно из предложений в тексте так:
    … большинство .htaccess файлов в Apache, которые я написал для своих сайтов, имеют в себе строчки...


    1. pda0
      07.09.2016 17:24
      +10

      Не, двойной трафик от Chrome, а вот двойной INSERT уже от собственных кривых рук.


    1. mmman
      15.09.2016 10:51

      Это в общем-то стандартный конфиг Wordpress, на котором крутится до 25% сайтов в мире.


  1. ErickSkrauch
    07.09.2016 17:26
    +20

    Не совсем понимаю: запрос на favicon делает через GET. Вставка данных, в современном мире, делает через POST. Мне кажется, автор сам себе прострелил ногу.


    1. sad
      07.09.2016 17:33

      Может у них там статистика какая-то своя считается по заходам на страницу, например.


      1. ErickSkrauch
        07.09.2016 17:34

        И по загрузке статики?


        1. wispoz
          07.09.2016 17:36
          +1

          Если все запросы заворачиваются на Index.php то да, халтурно настроенный апач) в вверху же написали.


  1. pudovMaxim
    07.09.2016 17:26
    +6

    У меня лыжи не едут.
    Почему две вставки происходит? Хром обратится GET-запросом напрямую к /favicon.ico. Никаких данных в POST или GET быть не должно. С чего вдруг двойное выполнение insert? Тогда получается любой 404ый запрос с этой страницы может косячить, даже аякс?


    1. FeNUMe
      07.09.2016 20:57
      +1

      Автор кривым .htaccess завернул все 404(включая статику) на index.php в итоге получил многократное выполнение основной логики.


  1. zodchiy
    07.09.2016 17:37
    +21

    Как укусить себя за задницу, для чайников. Издание 19283712893, переработатнное и дополненное.


    1. gearbox
      07.09.2016 17:55
      -7

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


  1. TrogWarZ
    07.09.2016 17:58
    +5

    > Внезапно я обнаружил, что на новом сайте, который у меня сейчас в разработке на локалхосте, дублируются INSERT запросы к БД. Отправляю один комментарий через форму, а в базу вставляются два.

    Реквестирую подробности рассказа о том как комментарии в базу вставляются по GET-запросу да ещё и без дополнительных данных.

    А вообще, нагрузку увеличить Хром и правда может не только как тут в комментах уже писали о user-friendly 404, но и при наборе в адресной строке конкретного адреса: prefetch-логика шлёт два GET-запроса, что дважды загружает страницу, инкрементирует дважды счётчики, даже может сбросить выделение непрочитанных данных в некоторых случаях.


    1. Akdmeh
      08.09.2016 19:54

      Не узнавали, как от этого избавиться? Жутко бесит при тестировании.


      1. TrogWarZ
        09.09.2016 07:22

        Chrome: Settings –> Privacy –> Use a prediction service… (снять оба чекбокса)


  1. andreymal
    07.09.2016 17:58
    +5

    Помимо вышенаписанного, это что же получается — автор перед тем, как отправиться гуглить, даже не заглянул в access.log? Ну или в мониторинг сети в самом хроме, в конце-то концов.


  1. el777
    07.09.2016 18:03
    +11

    Все верно: сделал говносайт через ж… — получи траф.
    Вы мне лучше расскажите, почему попадание на несуществующую страницу увеличивает счетчик заходов? Пользователь никуда не попал (404 == страницы нет)? Какая в этом логика подсчета? Хотите редирект — ок, перенаправьте и там засчитайте.

    Завтра к вам зайдут с iPhone/iPad и он запросит картинку apple-touch-icon.png. Еще строчку добавите в .htaccess?
    Вот где проблема: вы не хотите исправить кривое решение, а хотите обставить костылями по кругу.
    Жду еще 20 статей такого же рода, про каждый новый тип картинки.


    1. zapimir
      07.09.2016 18:55

      Ну в принципе 404 ошибки тоже неплохо считать, можно выловить неправильные ссылки и т.п.


      1. el777
        07.09.2016 19:17

        Да, и поэтому их надо считать отдельно.


  1. POPSuL
    07.09.2016 19:11
    +2

    Вся проблема кроется в Crome


  1. medved6216
    07.09.2016 19:22
    +1

    1 апреля уже? Пятница 13? Вы поржать или попугать? :) Без исходников вашего велосипеда(«движка») и настроек апача(.htaccess), тщательно проведенного анализа — кто сюда это пропустил?
    А по делу:
    1. плохо настроен modrewrite (10%);
    2. Ошибка в написании велосипеда, а именно в обработке «404 Not found» — скорее при обработке передаются все параметры запроса и если автор обрабатывает параметры в одном месть, то параметр на событие повторяется (например, index.php?act=comment) — не обязательно get, скорее всего запрос обрабатывается, через Request.


  1. handicraftsman
    07.09.2016 20:36

    В firefox почти такая-же проблема. Там дублируется запрос лишь к favicon.ico.


    1. handicraftsman
      15.09.2016 12:03

      Интересно, что проблема эта — кроссерверная. Обнаруживается даже на моём самописном сервере.


  1. tonissimo
    07.09.2016 20:46

    Помимо всего вышеописанного, стоит добавить, что заголовки Last-Modified, ETag, Cache-Control и Expires — для зануд, пусть поисковые роботы сами проверяют когда у меня контент обновится.


  1. Lure_of_Chaos
    07.09.2016 21:02
    +5

    Ждем серию постов от ТС о проблемах, возникших после того, как сайт стал виден из WWW…


  1. LeonidZ
    07.09.2016 23:35
    +3

    Меня больше потрясло то, что вы по сути по любому урлу без проверки, что именно вообще было запрошено, делаете инсерт в базу! Просто клондайк для DDOS. А как же css, js и другая статика?
    Статья должна называться: «как я несколько лет косячил с настройками apache и не проверял входящие данные в PHP».
    Я открою вам секрет: не только в Chrome так. Многие браузеры во время совершения ajax запроса шлют сначала OPTION запрос для принятия заголовков Allowed-*, например.
    Пересмотрите свое отношение к написанию кода, я прямо чувствую там дуршлаг )


  1. kamilsk
    08.09.2016 10:44
    +2

    Не поставил favicon на сайте — написал статью на хабре

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

    Еще в Developer Tools есть замечательная вкладка Network, где можно посмотреть все запросы, которые отправляет браузер


  1. AleXP3
    08.09.2016 10:44
    +1

    Интересно: вы любой запрошенный файл, который не нашелся на вашем сервере, планируете в .htaccess вносить?


  1. xakepmega
    08.09.2016 10:44
    +2

    А потом появляются вопросы почему все смеются над phpшниками…


  1. wtf_object
    08.09.2016 10:44
    -1

    Кто-то еще пишет sql запросы руками? :(


  1. webmoder
    08.09.2016 10:51

    Соглашусь с комментаторами выше, почему на 404 GET запросе происходит insert?


    Если вы пишите свою систему статистики посещений, то:


    • во-первых не стоит записывать каждый HTTP запрос (Это уже не статистика а логгер, который легко настроить на HTTP сервере).
    • во-вторых исключите обработку своим index.php запросы на статику по паттернам (прим. /assets/*, /favicon.ico, /robots.txt, /sitemap.xml), даже если файлов таких нет, в дальнейшем вы избежите подобных проблем.
    • в-третьих никакого двойного трафика нет, у вас так-же происходят запросы на получение js, css и т.п

    .З.Ы. Помню была немного похожая ситуация с хромом:
    Писал платежные шлюзы которые тестировались через GET запрос аля: http://{ip}/gatewayName.php?params....
    через какое-то время обнаружил что один шлюз начал переодически самопроизвольно запускаться, после долгого исследования обнаружил что хром добавил url в стартовую панель и переодически пытался обновить preview страницы.


  1. IlyaMoiseev
    08.09.2016 11:06

    Коллеги, спасибо за то, что уделили внимание и обсудили мою статью. Спасибо за все плюсы и минусы. Некоторые комментарии оказались действительно полезными — я не считаю себя «гуру» программирования и стараюсь развиваться.

    На язвительные комментарии отвечу, что речь в статье шла о необычном поведении браузера Chrome (другие браузеры не особо докапываются до отсутствия иконки) и о том, к чему это может привести в случае некорректно настроенного .htaccess на сервере. (А он такой по дефолту во многих open source движках, например Wordpress, так что думаю, проблема может оказаться распространенной). Например, я бы ожидал столкнуться с отсутствующими файлами в upload директории сайта, ну или в папке с темой оформления, но забыл бы про корень сайта. Статья никоим образом не должна иллюстрировать грамотные подходы к созданию современных сайтов.

    А про SELECT — тестировал новый метод класса на локалхосте. Сознательно вбил его вызов напрямую в index.php и в браузере F5, F5, F5… Только ради теста. Не надо придавать этому значения.


  1. pudovMaxim
    08.09.2016 12:05

    Кстати, это уже было (в сиспсонах) на хабре: https://habrahabr.ru/post/140693/


  1. LeamasRein
    08.09.2016 12:52
    -1

    Не буду разводить на холиваров на тему апача в 2к16, лампа на локалке дело обычное, но действительно пугают следующие вещи:
    — конфиг mod_rewrite;
    — INSERT в БД по GET запросу;
    — INSERT в БД без CSRF;
    — определение причинно-следственной связи.

    Школьнику/студенту еще простительно, но 5 лет разработки в вебе?

    ps. Почитал комментарии. Товарищам, считающим, что ТС добавлял записи для счетчика просмотров — прошу прочитать первый абзац в статье чуточку внимательнее, где явно указано про форму комментариев. Хотя и с прямым взаимодействием (вставка/обновление) с БД при реализации счетчика просмотров я бы поспорил.


  1. G-M-A-X
    08.09.2016 22:02

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

    А конфиг-то дефолтный, наверное, для большинства систем с единой точкой входа :)


  1. UksusoFF
    12.09.2016 10:04

    Такая же проблема с background-image: url() если картинка отсутсвует.


    1. G-M-A-X
      13.09.2016 00:41

      Такая же проблема с любым запросом к отсутствующему ресурсу :)


      1. UksusoFF
        16.09.2016 20:28

        Ну почти, если ссылка на картинку не правильная, то да.
        А если там по какой-то причине внезапно оказалось background-image: url(''), то не совсем очевидно :)


        1. G-M-A-X
          16.09.2016 21:12

          Хм, но это все равно, что:

          <a href="">link</a>
          

          — ссылка на текущую страницу (если такое написать в css файле, то это ссылка на css файл)


          1. UksusoFF
            16.09.2016 21:18

            Да, но не все помнят.