11

I have encountered baffling behavior when running an Ajax request as part of my Flask application. I have written a handler receive a div click and then send an Ajax request with certain data to a specific route specified in my app.py. The data is then inserted into a database. While this approach worked fine when running my Flask app on my own machine, upon moving my app to another hosting service (Pythonanywhere), every time I click the div, the request is being sent twice, as evidenced by the data being inserted twice into the database.

Similar variants of this question have been asked before (here and here, for instance), but those questions all deal with POST requests, while mine is using a GET. Additionally, those questions generally involved an HTML form that was being submitted alongside the POST request, hence the additional request. However, my code does not have any forms.

My code sample (simplified, but the same in essence to my current efforts):

In frontend.html:

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

In 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);
     },

   });
});

In 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>'})

As such, whenever I click the submit_stamp element, the Ajax request fires twice, the timestamp is inserted twice into my database, and "Added timestamp" is appended twice to .wrapper. Some things I have done to fix this include:

  1. Adding an event.stopPropagation() in the handler

  2. Using a boolean flag system where a variable is set to true just after the click, and reset to false in the success handler of the .ajax. I wrapped the $.ajax with this boolean in a conditional statement.

None of these patches worked. What confuses me, however, is why $.ajax is called once when running on my machine, but is called twice when running on the hosting service. Does it have to do with the cache? How can I resolve this issue? Thank you very much!

Edit:

Strangely, the duplicate requests occur infrequently. Sometimes, only one request is made, other times, the requests are duplicated. However, I have checked Network XHR output in Chrome and it is only displaying the single request header.

The access log output (with IPs removed):

<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
Ajax1234
  • 69,937
  • 8
  • 61
  • 102
  • 1
    Maybe your `frontend.js` is getting loaded twice (or somewhere else), registering two click handlers. Try doing `$('.wrapper').off('click')` before the code (although that will remove all handlers) – MinusFour Aug 03 '19 at 02:31
  • you getting any console error after first request? or try to run it from console for debugging – Ravi Aug 03 '19 at 04:47
  • @RV I did not receive any errors during the entire process. I added a `console.log` in the `success` handler which only printed once, however. Do you know what this could mean? – Ajax1234 Aug 03 '19 at 04:58
  • you sure. ajax is calling twice. have you seen in network?? – Ravi Aug 03 '19 at 05:01
  • On the server logs, do you see the request coming twice and from the same IP? – Tarun Lalwani Aug 05 '19 at 16:58
  • @TarunLalwani Strangely, the requests are coming from two different IPs. Please see my recent edit, as I added the log output. – Ajax1234 Aug 05 '19 at 17:05
  • So may be you are saying its repeated by they are coming from different places as such? – Tarun Lalwani Aug 05 '19 at 17:07
  • @TarunLalwani Yes, that is what it looks like from the logs. – Ajax1234 Aug 05 '19 at 17:08
  • maybe you are double-clicking? Try to setup other event, like `keydown` or `keyup` – Valijon Aug 05 '19 at 17:51
  • @Ajax1234. Can you try to moniter ajax request from console using chrome once? https://www.codexworld.com/how-to/monitor-ajax-requests-google-chrome/. See if its calling twice or not. – Ravi Aug 09 '19 at 04:52

5 Answers5

4

With your latest update, I'd have to say this isn't a duplicate request. With your log saying one request was from Mozilla on a Windows based machine, and the other request coming from Chrome on a Mac, it's simply 2 different requests coming from two different locations that happen to be close to each other in time. Even if it was a test from a virtual machine, it shouldn't record the multiple OSs or browsers, as VM will take care of all translations, preventing confusion like this.

You don't include IP addresses, but if they are public addresses (as in something other than 127.x.x.x, 10.x.x.x, or 192.x.x.x) then they are definitely two different users that happen to be using your software at the same time.

If you are tracking that it's the same user, it might simply be them using your software on 2 different devices (such as a desktop vs mobile phone). If that's not allowed, then make sure their access reflects this. If it can be tracked through DNS to different geographical locations, you might have a compromised account to lock down until the real user can confirm their identity.

However you slice it, with the new data, I don't think it's actually your software, unless you can reproduce it through testing even somewhat reliably. Take time to consider that it might just Not be a bug, but something else. Software devs are conditioned to think everything is a bug and their fault, when it could be something benign or a malicious attack that might not have been previously considered.

Good luck and hopefully I gave you something to think about!

computercarguy
  • 2,173
  • 1
  • 13
  • 27
  • Thank you very much for your post. Indeed, I have set up smaller code samples on my server, and I cannot replicate the duplicate issue that appears on `/submit_time`. What I do find strange, though, is how quickly the second request is fired (2 milliseconds later). Could there be some process that is "mirroring" my original request? – Ajax1234 Aug 05 '19 at 17:45
  • @Ajax1234, if there was a "mirrored" request, I'd have to say it's likely to have the same IP address as well as browser and OS signatures. This could be an inept "man in the middle" attack of a user that is allowing the original request as well as another request coming from their site to fake the data. I just don't see how this signature change could be caused by your software. Even a router that's mis-routing would only send a single request, and it still wouldn't change the signature like that. – computercarguy Aug 05 '19 at 17:53
  • @Ajax1234, you mention that's it's kind of rare for this to happen. You might want to check to see if there really are 2 or more users actively using your software at the same time. It's not common, to have key presses or clicks at the same time in testing, but if you have 50-100+ simultaneous users, it might just be happening. – computercarguy Aug 05 '19 at 18:00
3

Thank you to everyone who responded. Ultimately, I was able to resolve this issue with two different solutions:

1) First, I was able to block the offending request by checking the IP in the backend:

@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>'})

The above is really more of a temporary hack, as there is no guarantee the target IP will stay the same.

2) However, I discovered that running on HTTPS also removed the duplicate request. Originally, I was loading my app from the Pythonanywhere dashboard, resulting in http://www.testsite.com. However, once I installed a proper SSL certificate, refreshed the page, and ran the request again, I found that the desired result was produced.

I am awarding the bounty to @computercarguy as his post prompted me to think about the external/network related reasons why my original attempt was failing.

Ajax1234
  • 69,937
  • 8
  • 61
  • 102
2

Very unusual solution, but it should work (If not, I think the problem can't be solved with js.)

EDITED: Check the sent ID in the ajax request! (So check on server side!) This is sure will be a unique id, so, you can test with this @computercarguy has right or not.

let ids = []

function generateId(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)
});

function ajaxHandler(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", "")
    }
});
danigore
  • 411
  • 1
  • 5
  • 8
0

So first of all I would use below syntax as a personal preference

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

Also as I said, you need to make sure when you refer to two concurrent request, they are indeed from same IP+client, else you may be confusing between parallel request from different places to be repeated as such

Tarun Lalwani
  • 142,312
  • 9
  • 204
  • 265
-2

Little change in your js file.

$('.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);
     },

   });
});
Amit
  • 2,018
  • 1
  • 8
  • 12