Ajax GET Запрос отправляется дважды

Я столкнулся с непонятным поведением при выполнении Ajax-запроса как части моего приложения Flask. Я написал, что обработчик получает щелчок div, а затем отправляет запрос Ajax с определенными данными на определенный маршрут, указанный в моем app.py. Затем данные вставляются в базу данных. Хотя этот подход работал нормально при запуске моего приложения Flask на моей собственной машине, при перемещении моего приложения в другую хостинговую службу (Pythonanywhere) каждый раз, когда я нажимаю div, запрос отправляется дважды, так как Свидетельством тому являются данные, вставляемые дважды в базу данных.

Подобные варианты этого вопроса уже задавались (например,здесь и здесь), но все эти вопросы касаются запросов POST, в то время как мой использует GET. Кроме того, эти вопросы обычно включали HTML form, который был представлен вместе с запросом POST, и, следовательно, дополнительный запрос. Однако в моем коде нет форм.

Пример моего кода (упрощенный, но по сути такой же, как мои текущие усилия):

В frontend.html:

<div class='wrapper'>
   <div class='submit_stamp' data-timestamp='2019-8-2'>Submit</div>
</div>

В frontend.js:

$('.wrapper').on('click', '.submit_stamp', function(){
   $.ajax({
     url: "/submit_time",
     type: "get",
     data: {time: $(this).data('timestamp')},
     success: function(response) {
       $('.wrapper').append(response.html);
     },

   });
});

В app.py:

@app.route('/submit_time')
def submit_time():
   db_manager.submit_stamp(flask.request.args.get('time'))
   return flask.jsonify({'html':'<p>Added timestamp</p>'})

Таким образом, всякий раз, когда я щелкаю элемент submit_stamp, запрос Ajax запускается дважды, временная метка дважды вставляется в мою базу данных, и "Added timestamp" добавляется дважды к .wrapper. Вот некоторые вещи, которые я сделал, чтобы это исправить:

  1. Добавление event.stopPropagation() в обработчик

  2. Использование системы логических флагов, в которой переменная устанавливается на true сразу после щелчка, и сбрасывается на false в обработчике success в .ajax. Я обернул $.ajax этим логическим значением в условном выражении.

Ни один из этих патчей не сработал. Что меня смущает, так это то, почему $.ajax вызывается один раз при работе на моей машине, но вызывается дважды при работе на хостинге. Это связано с кешем? Как я могу решить эту проблему? Большое спасибо!

Изменить:

Как ни странно, дубликаты запросов происходят нечасто. Иногда делается только один запрос, в других случаях запросы дублируются. Однако я проверил вывод Network XHR в Chrome, и он отображает только один заголовок запроса.

Вывод журнала доступа (с удаленными IP-адресами):

<IP1> - - [05/Aug/2019:16:35:03 +0000] "GET /submit_time?payload=.... HTTP/1.1" 200 76 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.1) Gecko/2008070208 Firefox/3.0.1" "<IP>" response-time=0.217
<IP2> - - [05/Aug/2019:16:35:05 +0000] "GET /submit_time?payload=.... HTTP/1.1" 200 71 "http://www.katchup.work/create" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36" "<IP2>" response-time=0.198

Ответы

Ответ 1

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

1) Сначала я смог заблокировать ошибочный запрос, проверив IP-адрес в бэкэнде:

@app.route('/submit_time')
def submit_time():
   _ip = flask.request.environ.get('HTTP_X_REAL_IP', flask.request.remote_addr)
   if _ip == '128.177.108.218':
     return flask.jsonify({'route':'UNDEFINED-RESULT'.lower()})
   return flask.jsonify({"html":'<p>Added timestamp</p>'})

Вышесказанное - скорее временный взлом, поскольку нет гарантии, что целевой IP останется прежним.

2) Однако я обнаружил, что при работе по HTTPS также был удален повторяющийся запрос. Первоначально я загружал свое приложение с панели инструментов Pythonanywhere, что привело к http://www.testsite.com. Однако после того, как я установил соответствующий сертификат SSL, обновил страницу и снова запустил запрос, я обнаружил, что был получен желаемый результат.

Я присуждаю вознаграждение @computercarguy, поскольку его сообщение побудило меня подумать о внешних/сетевых причинах, по которым моя первоначальная попытка не удалась.

Ответ 2

С вашим последним обновлением я бы сказал, что это не дубликат запроса. В вашем журнале говорится, что один запрос был от Mozilla на компьютере под управлением Windows, а другой запрос поступил от Chrome на Mac, это просто 2 разных запроса, поступивших из двух разных мест, которые оказались во времени близкими друг к другу. Даже если это был тест с виртуальной машины, он не должен записывать несколько ОС или браузеров, поскольку ВМ позаботится обо всех переводах, предотвращая путаницу, подобную этой.

Вы не включаете IP-адреса, но если они являются публичными адресами (как, например, в 127.xxx, 10.xxx или 192.xxx), то это определенно два разных пользователя, которые используют ваше программное обеспечение одновременно время.

Если вы отслеживаете, что это один и тот же пользователь, это могут быть просто те, кто использует ваше программное обеспечение на 2 разных устройствах (например, настольный компьютер или мобильный телефон). Если это не разрешено, убедитесь, что их доступ отражает это. Если его можно отследить через DNS в разных географических точках, у вас может быть взломанная учетная запись для блокировки, пока реальный пользователь не сможет подтвердить свою личность.

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

Удачи и, надеюсь, я дал вам кое-что подумать!

Ответ 3

Очень необычное решение, но оно должно работать (если нет, я думаю, что проблему нельзя решить с помощью js.)

РЕДАКТИРОВАНИЕ: проверьте отправленный идентификатор в запросе ajax! (Так что проверяйте на стороне сервера!) Это наверняка будет уникальный идентификатор, поэтому вы можете проверять, имеет ли @computercarguy право или нет.

var ids = [];
var generateId = function(elem)
{
    let r = Math.random().toString(36).substring(7);
    while ($.inArray(r, ids) !== -1)
    {
        r = Math.random().toString(36).substring(7);
    }

    ids.push(r);
    elem.attr("id", r);
};

$(document).ready(function()
{
    $(".wrapper").find(".submit_stamp").each(function()
    {
        generateId($(this));
    });

    console.log(ids);
});

var ajaxHandler = function(stampElem, usedId)
{    
    let testData = new FormData();
    testData.append("time", stampElem.data('timestamp'));
    testData.append("ID", usedId);

    $.ajax({
        url: "/submit_time",
        type: "get",
        data: testData,
        success: function(response)
        {
            $('.wrapper').append(response.html);
            generateId(stampElem);
            if (stampElem.attr("id").length)
            {
                console.log("new id:"+stampElem.attr("id"));
            }
        },
    });

};

$(".wrapper").on("click", ".submit_stamp", function(ev)
{
    ev.preventDefault();
    ev.stopImmediatePropagation();

    if ($(this).attr("id").length)
    {
        let id = $(this).attr("id");
        $("#"+id).one("click",
            $.proxy(
                ajaxHandler, null, $(this), id
            )
        );
        $(this).attr("id", "");
    }
});

Ответ 4

Поэтому прежде всего я бы использовал приведенный ниже синтаксис в качестве личного предпочтения

$('.wrapper').click(function (event) {
    event.stopImmediatePropagation();
    $.ajax({
        url: "/submit_time",
        type: "get",
        data: {
            time: $(this).data('timestamp')
        },
        success: function (response) {
            $('.wrapper').append(response.html);
        },
    });
});

Также, как я уже сказал, вы должны убедиться, что при обращении к двум параллельным запросам они действительно относятся к одному IP + клиенту, иначе вы можете запутаться между параллельным запросом из разных мест, который будет повторяться как таковой

Ответ 5

Небольшое изменение в вашем файле JS.

$('.wrapper').on('click', '.submit_stamp', function(event){
   event.preventDefault();
   event.stopImmediatePropagation();
   $.ajax({
     url: "/submit_time",
     type: "get",
     data: {time: $(this).data('timestamp')},
     success: function(response) {
       $('.wrapper').append(response.html);
     },

   });
});