0

Users of my php application sometimes accidentially submit messages or other actions twice without wanting to. This seems to be an issue caused by (mobile) clients automatically retrying a request if the response takes too long, e.g. due to bad network connection. My server though handles both requests and processes them as seperate actions.

How can I handle such retry post requests without disabling the possiblity of sending deliberate double post requests? Therefore how can i distinguish between these retries and deliberatly user-send resends of the same request?

A bit of context, that could help understand the problem: I'm running a oldschool html-based browser game. Users may want to resend the exact same action like "attack" multiple times through page refresh delibaretly and I do not want to cut this option, as it's really convenient. But I want to prevent unintended double actions through clients automatically sending post requests twice without asking the user first. I'm not entirely sure if its a mobile browser specific problem.

dfherr
  • 1,633
  • 11
  • 24
  • 2
    Perhaps a little Javascript at the clients end? – Ed Heal Feb 19 '16 at 19:10
  • What's the difference between a mobile user re-sending the request because they didn't receive a timely response, vs. a "deliberate double post". Both actions sound pretty deliberate. – David Feb 19 '16 at 19:13
  • You want to implement some kind of one-time token. (That should do as an initial research keyword.) – CBroe Feb 19 '16 at 19:15
  • @David The difference is that a machine is reseding the request without asking the user and in the other request the user wanted to resend that request. In particular I'm running a browsergame. There are use cases there users want to redo a action multiple times fast by simply triggerering resend. But there are other situations this behaviour is actually hurtful. – dfherr Feb 19 '16 at 19:15
  • 3
    The first solution that comes to mind is to use a session variable. Possibly set the last post time, and if it's too soon you don't allow them to do it again. Seems the simplest solution. – ckimbrell Feb 19 '16 at 19:16
  • @CBroe i thought about one time tokens. But wouldn't that disable the option to trigger the resend delibaretly? I should consider asking "Whats the difference between a delibarte refresh (e.g. via F5) and a automatically resend request?" – dfherr Feb 19 '16 at 19:17
  • @ckimbrell it also came to my mind to check the time between two requests. The problem here is that deliberately user send post requests can (and should be allowed to) be faster as that automatically triggered resend through bad internet connection. – dfherr Feb 19 '16 at 19:24
  • Simply disbale the submit button via javascript once it has been clicked. If you like you can show some kind of "sending request" overlay o the user knows whats going on. – maxhb Feb 19 '16 at 19:29
  • @maxhb the problem is not users sending a request twice on purpose. This happens automactically, even without reconfirmation. So the problem is not something the user does, but what his client does by himself. I'm not 100% sure that is whats happening, but it's the only logical explanation for me how this happens. It happened to myself using chrome on android, but reproducing it on purpose is kind of impossible for me. The user then gets the repsonse to his second request shown, but with database changes from 2 requests. I was kind of hoping someone had a similar problem and a good solution – dfherr Feb 19 '16 at 19:36
  • 2
    OK, I understand. Have you read this quite enlightning article? http://stackoverflow.com/questions/14302512/what-happens-when-no-response-is-received-for-a-request-im-seeing-retries – maxhb Feb 19 '16 at 19:43
  • Maybe have your processing page set a session variable that it has been processed, then forward them to a "thank you" page that unsets it. This way if they are reloading in the middle they are caught, if they hit the thank you page then they can go back and send another without issue because they have completed the process. – ckimbrell Feb 19 '16 at 19:48
  • @ckimbrell as far as i can tell this forwarding and then going back would work for firefox, but chrome on windows does not redo post requests on the back action. Therefore this solution would disable the refreshing feature for chrome users. Firefox users would definitly lose a lot of usability, if they have to go back and confirm a resend instead of just confirming a resend. – dfherr Feb 19 '16 at 19:57
  • I'm not sure I follow, or possibly my solution wasn't clear. My example would have 3 pages. 1 - your form. 2 - processing page that sets session variable. 3- thank you page that unsets session variable. Maybe your third page then forwards them back to page 1 and they can submit the form again. The only issue would be if a user was trying to use the browser's back button. Are you saying they can just refresh to send again and that is acceptable? – ckimbrell Feb 19 '16 at 20:00
  • 1
    @ckimbrell Yes, just refresh and send again is acceptable and really convenient in a lot of occassions in the game (e.g. you fill in the attack form with your units and want to attack the target 3 times with that composition). At least Chrome and FF prompt you to confirm that you actually want to resend, so unintended user resends are not an issue. Unintended client retries though are. – dfherr Feb 19 '16 at 20:04
  • Well, you could then use session variables to pre-fill the form when returning to the page after the loop of setting / unsetting session variables. They wouldn't necessarily be able to refresh but could continually hit send. I'm still thinking on the "refresh" aspect of it... – ckimbrell Feb 19 '16 at 20:07
  • @ckimbrell that would be a working solution to my problem that came to my mind. The issue with that is that it's a lot of work to circumvent a different problem xD but you are right, it would certainly solve the issue, but i was holping to get a simpler/less time consuming solution (it's a non-profit project). At the moment i guess it comes down to somehow distinguish between user-send retries and machine-send retries and then use your suggested session based time difference. I should search for answers to that and/or open a question for it. According to maxhb link this retries are standard. – dfherr Feb 19 '16 at 20:18
  • _“But wouldn't that disable the option to trigger the resend delibaretly?”_ – you could implement a step to inform the user that the token has been used already then, and generate a new one and present them with the same pre-filled form again – so if the user _deliberately_ wants to submit the same data again, they can. (And if you’re afraid people would do that to often to spam your site – then force them to fill out a CAPTCHA first maybe, or implement an additional timer with your token, so that they can only submit the same form again after time interval X.) – CBroe Feb 19 '16 at 20:35
  • You might be able to make use of some of the testing parts added to my answer on this question http://stackoverflow.com/questions/34376536/stop-multiple-submits-for-a-html-from/34377412#34377412 so that you could set up a "Did you mean that?" response based on the count of hits received. You could also add that at the JavaScript end of the code here. – Steve Feb 19 '16 at 21:31
  • @Steve i see the question was unclear. It's more about automatically triggered retries by the user-client not about users retrying the request. – dfherr Feb 19 '16 at 21:44
  • 1
    Yes I do understand that but if each submission is held until the user confirms they meant to do it wouldn't that help - So if it was submission #1 and you were suddenly looking at submission #3 you could prevent #3 having an effect (assuming you ever received the "actual" submission!). http://stackoverflow.com/questions/15155014/inconsistent-browser-retry-behaviour-for-timed-out-post-requests is along similar lines to the excellent articles referred to above. I would be interested to know, if you throw the code from my link into a php page, what counts it returns in your Android browser – Steve Feb 19 '16 at 21:57
  • 1
    The JavaScript on http://stackoverflow.com/questions/34376536/stop-multiple-submits-for-a-html-from/34377412#34377412 can be used to increment the count, so that, unless the count is higher than it was (i.e. it was not the result of having passed via the submit button which fires the JavaScript function) then you reject the submission. The JavaScript value is submitted via the hidden input which is set with `document.GetElementByID()`. – Steve Feb 19 '16 at 22:28
  • @Steve thanks, at first glance it looks like a solution. I will look into it tomorrow. Thing is, i want the user to be able to submit the request by triggerering a page reload and not to have them fill out the form again, because my forms (plural) do not retain their value and thats quite some work and it's a non-profit project ;) Refilling the submit forms with the previous request values and adding some kind of tocken/counter seems like the best solution, though it disables the F5 intended resend method. – dfherr Feb 19 '16 at 23:16
  • 1
    I was writing the subdown as you answered so I thought I would publish it anyway - hope it may be of some use. Just throwing the original page into a `.php` page should give an idea of whether it is in the ballpark to avoid wasting any time - I did test that page. – Steve Feb 19 '16 at 23:33
  • Sorry to hear that - best of luck to all. I look forward to hearing in due course. – Steve Feb 21 '16 at 21:51

2 Answers2

1

You are focussing on the wrong end of the problem. If users are sending the post twice then its probably because they are not seeing a response at their end. Yes, you still need to plan for and handle the problem serverside, but addressing it on the client will not only reduce the number of occurrences getting back to your server, but also improving the user experience.

A real-time update at the front end means JavaScript. Also, when you're working with touch devices there is, by default, a latency of around a third of a second unless you explicitly disable some of the touch functionality.

symcbean
  • 47,736
  • 6
  • 59
  • 94
  • it's not the users who send the post request twice, it's their client who automatically sends the request a second time. i changed the question once more to be more clear – dfherr Feb 19 '16 at 21:42
  • What you are claiming is very unlikely. – symcbean Feb 19 '16 at 22:46
  • this automatic retry behaviour is actually standard and described in HTTP/1.1 RFC 8.2.4 as people linked me in the comments. Though with mobile clients it seems to happen a noticeable amount of times that two post requests actually reach the server and get processed. – dfherr Feb 19 '16 at 23:14
  • @dfherr This is on the right track though, and something well worth considering in terms of your "user experience" especially if there is a known issue with slow mobile connections. Maybe even an "Easy tiger, hang on a mo!" alert if you detect it has not submitted yet! – Steve Feb 20 '16 at 00:05
0

I believe this should do what you want it to and it should at least give you the idea - it was originally part of a larger post Stop multiple submits for a html from regarding the prevention of actual double submission.

Looking at it again, expecting you to sub that yourself might be too much of an ask, so here is a subbed version of it. You may want to re-implement the disabling of the submit button but it seems you actually want it to be enabled continuously so I have removed that code here for the sake of clarity.

You may need to adjust the counts - not sure how many multiple automatic submissions might occur - as it stands this would account for one, but after that it will not accept anything until the next "real" submission.

At that point the PHP code could request user intervention along the lines of "Did you really submit that twice?" or it could simply ignore the submission. The same is true for the JavaScript which can ask for confirmation before it re-submits at all, as per @symcbean's suggestion - belt and braces always a good idea! If you click while it is still submitting you should get the option to submit twice deliberately, or cancel.

As far as I can tell in this version, if you comment out the keyPress event submit_this(); call for an F5 key press, it will not accept an F5 submission for a refresh. - When the keyPress function is in action, pressing F5 submits without the usual confirmation warning you would get for a refresh.

I have test run this but can only wait for your confirmation to see if it copes with automatic re-submission as well. If you click the browser refresh button the submission is not accepted in the PHP. The submission resulting from a Ctrl+R refresh is also not accepted. Hopefully this mirrors the re-submission scenario. I have put back the alert for a faster click than the page was being submitted at as it was not possible to tell whether the confirm/cancel was working. You could experiment with that!

<?php   
session_start();
if (!isset($_POST['hidden_input'])) $hidden_input = 1;
if (!isset($_SESSION['submit_count_input'])) $_SESSION['submit_count_input'] = -1;
if (isset($_POST['hidden_input'])) $hidden_input = intval($_POST['hidden_input']);

// detect difference between JavaScript submitted value and possible automatic ones
if (($hidden_input - $_SESSION['submit_count_input']) == 1){
    // do your processing here 
    //$submit_count_input should always be 1 behind
    echo "Thank you. Form processed";
    }else{
    // correct the balance between $hidden_input and $submit_count_input
    $_SESSION['submit_count_input'] = ($hidden_input - 1); // will be incremented so will be -1 effectively
    echo "Form not processed.";
}    
// remove this echo section - just for testing
echo " Control Token: " . $hidden_input . " ";
echo " Submit Count: " . $_SESSION['submit_count_input']; // to show it submits but does not process form

$_SESSION['submit_count_input']++; 
?>
<!DOCTYPE html>
  <html>
    <head>
    <script language="javascript">
    document.onkeydown=function(e) {
        var event = window.event || e;
        if (event.keyCode == 116) {
        event.keyCode = 0;
           submit_this();
           return false;
        }
    }

function submit_this(){
    if(document.getElementById('submit_count_input').value - document.getElementById('hidden_input').value == -1) {
        // Warning when submit clicked before previous submission finished confirm/cancel option as per link below could be used
    alert("Easy Tiger - please slow down a bit!");
    }else{
    document.getElementById('hidden_input').value = 1+parseInt(document.getElementById('hidden_input').value);
    document.form.submit();
    }
}
    </script>
    </head>
    <body>

      <form name="form" id="form" action="<?php echo htmlspecialchars($_SERVER['PHP_SELF']) ?>" method="post">   
      <input type="hidden" name="hidden_input" id="hidden_input" value="<?php echo $hidden_input ?>" />
      <input type="hidden" name="submit_count_input" id="submit_count_input" value="<?php echo intval($_SESSION['submit_count_input']) ?>" />

      <input type="button" name="sbutton" id="sbutton" value="Submit" onclick="submit_this();" />
      </form>

      <a href="<?php echo htmlspecialchars($_SERVER['REQUEST_URI']); ?>">Click to start a new submission</a>

    </body>
   </html>

Confirm/Cancel JavaScript dialogue box JavaScript confirm cancel button not stopping JavaScript

With regard to the secondary issue of keeping the values in the submitted forms, without sessions, then that should be fairly straightforward even for a lot of forms if you use value="<?php echo htmlspecialchars($_POST['form_element_name_value'] ?>" in each form text box element's value. Make sure each text box has a name="something" for php to pick up as a $_POST - sorry if that is too obvious.

To address the desire for your users to be able to use F5 to submit the page you could capture the functionality and subvert it to an actual submit, but that is probably a nasty idea though it does work...and it would keep the PHP side fed with submitted data which could possibly still be distinguished from automatic resubmissions for failed connections.

    document.onkeydown=function(e) {
        var event = window.event || e;
        if (event.keyCode == 116) {
        event.keyCode = 0;
        submit_this();
        return false;            }
    }

To capture the actual refresh button on the browser you could look at this How to know whether refresh button or browser back button is clicked in firefox but subverting browser behaviour to force a given user experience is something which is generally frowned upon.

Community
  • 1
  • 1
Steve
  • 808
  • 1
  • 9
  • 14
  • hey thanks for your detailed answer. i had an accident in the family and won't be able to test if this works the next few days. but i'll definitely come back to and am grateful! – dfherr Feb 21 '16 at 18:01