Avoiding duplicated form submissions

Note: this entry has moved.

 

Fast Joe

 

Last week I was asked to add some support to our customized ASP.NET framework to help developers with the common Fast Joe scenario: an impatient user clicking the submit button 3 times in less than a second thus causing multiple submissions of the same form which is usually a very bad thing (i.e. duplicate processing, etc).

 

Googling around

 

Having not approached this problem before I started googling around to learn how others were dealing with this. That’s how I first got to this post from Nicole Calinoiu (don’t know if she has a weblog) summarizing common approaches along with their pros/cons. From there I got to some more advices from Nicole, this time commenting a post from Andy Smith. And finally, a later post from Andy about a custom control he wrote -named OneClick- apparently inspired by Nicole’s comments.

 

Flawed OneClick?

 

By quickly looking at OneClick’s source code I’m noticing what I believe is a big flaw. The control offers a boolean IsValid property whose description in the docs read: “…true if the user only submitted the form once, false if more submittals were received…” and it recommends that you write your code this way:

 

if (OneClick1.IsValid) {

     // Code here is important to happen only once, and might take substantial amounts of time to complete

     // For example purposes, the thread will be put to sleep for 3 seconds.

     System.Threading.Thread.Sleep( TimeSpan.FromSeconds(3) );

}

 

in order to avoid running the code multiple times if the first request is still processing.

 

OneClick’s main logic consists in generating a key (Guid) and sending it down to the client in a hidden form field. Then, on postbacks, the control will look for this key (at control initialization time) and store it into Cache (yes… this will break webfarm scenarios) if present. At control unloading time the added key is removed from Cache. Note how this key is being used as a flag to signal the ongoing execution of a request. The IsValid property will be set accordingly: if the key is present it means the original request has not completed processing yet, so IsValid should be set to false indicating we’re dealing with a duplicate submission. Although this may sound well it’s missing an essential ASP.NET architecture fact: execution of pages accessing the same session state data is serialized, meaning IsValid property will never get a chance to be false in pages having session state enabled. Ouch!.

 

Now, let’s suppose it was designed for use in pages not accessing session state. The first submission will cause the flag to be set and then will start processing the code guarded by the IsValid check. Meanwhile the user resubmits the form, this time causing the new request to be processed concurrently. Now IsValid will be properly set to false so code guarded by it won’t execute again for this 2nd submission. Sounds good so far? Wait… Fast Joe decides to resubmit again while the original submission is still taking place… and for this 3rd submission IsValid will be… true! Why? Because OneClick’s unloading code has already cleared up the flag when it had a chance to run for the 2nd submission. Ouch!

 

As I said I’ve just browsed through the code so I need to find some time to compile it and play with it a bit to check my assertions.

UPDATE: I've done some quick & dirty testing and have confirmed every assertion.

  

Going client-side or server-side?

 

It looks to me like a client-side approach would be the best solution (based on the specific requirements I’m dealing with). The only problem is that our framework must also support mobile (read non javascript capable) devices which I guess calls for a hybrid solution. When I find the time to tackle this (it’s not currently on my 50-TO-DO list…) I will blog again detailing the approach taken. Meanwhile, your feedback on the topic is much appreciated :--)

 

12 Comments

  • Yes, OneClick does have some problems. However, I believe them to be implementation problems, and not theroy problems.



    I think that the problems would go away if I removed the unload handler which removes the guid from the cache. I originally had that there in an effort to lower the amount of memory used by storing form guids, but obviously didn't thinking about the "3+ clicks in a row" issue thuroughly enough.



    Simple testing on my local server showed that clicking twice on the button did indeed cause IsValid to return false.



    Victor, email me or IM me if you think this is wrong or you just want to chat about it.

  • Another note...

    I put OneClick together in about an hour that nite, after talking about it with nicole, on and off, during the day.



    1) "Seems like approach X should work"

    2) code

    3) "Works for me"

    4) post



    I probably should have gotten in a few more discussions with more people.



    Note that OneClick doesn't really work on WebFarms either, because of seperate Cache stores in each server's memory, unlike Session.

  • Just seeing Thread.Sleep in a web page gives me the chills.



    I think that this is a client (browser) issue.

    Don't mobiles support some limited scripting?



    If I click the button on the example twice quickly, it seems to think it has only been clicked once, but this might be a annother browser issue.



    If a page is changing data and it takes a long time to do it's processing, it may be a good idea to send some content before it has finished by Flush-ing the Response.









  • I was talking with some coworkers about this topic just the other day. I'm of the opinion that the proper way to handle this is on the backend.



    Consider a traditional B2C site where clicking the final order button charges my card and places the order. The page submitting the order should have an order key submitted with the page either through the VIEWSTATE or some other hidden form field. It should also encrypt the information, which can be done via the VIEWSTATE.



    Server side, you persist the the fact that the order has been started (possibly beginning a workflow), using the order key from the VIEWSTATE. If your insert fails due to a duplicate key, then you know the user had submitted the form multiple times and can take appropriate action.

  • Andy: yes clicking only twice in pages having session state disabled should work, clicking more should break it; it should break all times in pages with session state enabled I believe. And yes, I've noticied it will break on webfarms, too bad for my scenario :-( (shared session is not an option for me either).



    Andrew: Agreed with the example (it was just copy & pasted from OneClick's doc) but this doesn't apply exclusive to lengthy operations (which should be addressed differently btw) but to anything that takes a sec or two and may cause Fast Joe to resubmit the form.



    I also prefer to tackle this at the client-side. Some mobiles do support full javascript, others only a subset and other have no support at all. Also, old browsers used in platforms like QNX doesn't support javascript.



    Thanks you both for your comments,

  • Kevin: I'm more inclined to handle it at the client-side (whenever possible) as it saves a lot of unnecessary processing (handling additional useless request, saving bandwidth, eliminates checking code, etc) but I'm aware I won't have JS running on all our supported devices so I will definitively need to design a hybrid solution.



    Thanks for commenting,

  • "it should break all times in pages with session state enabled I believe"



    I don't think so. Removing the UnLoad handler will cause the guid to remain in the cache for the duration of a page timeout.



    If I were to increase that even more... to say... 5 minutes (WARNING: ARBITRARY NUMBER ALERT), then it seems to me that even if the requests are serialized, after the long process is done and the second or third click postbacks are processed, then the guid will be in the cache and IsValid will return false.



    Am I missing something?

  • Re: "breaking at all times"; I was referring to your original implementation which should break always on pages with session state enabled.



    I'm reading now your suggestion about removing the OnUnload handler. I'm afraid that won't work either as you're still generating a new key everytime OnPreRender runs which means the 2nd submit may return the new key and a 3rd submit -while the first one is still processing- will be handled as an original submit causing trouble again.



    What may fix it: in OnPreRender register the original key if isValid is false (thats what you want, to return the same key *only* if the original submit isn't finished yet) or register a new key if isValid is true. Also, leave the OnUnload handler but only clear the key from cache if isValid is true, that should avoid invalidating legitimate original submissions that happen before the timeout you're specifying.



    Again, I'm thinking this of the top of my head so you may wanna double check everything.

  • I tried some tests with the nUnit asp.net extension, but it insists on waiting for the response after buttonTester.Click().

  • I've just done my own quick and dirty testing and was able to corroborate 100% my assertions. Also, my recommended fix seems to be playing ok...

  • It seems to me that the serialized execution of pages accessing the same session state would actually be an advantage in dealing with Fast Joe on the server side.



    Since by definition multiple requests from the same user are processed in the order in which they're received, a token might not even be necessary; a simple flag might do the trick. Before the one-time-only code executes, check to see if, for example, Session("Form1_Started") = True. If not, set it to true and execute the one-time code.



    Now the part I'm having trouble with: What should happen if Session("Form1_Started") isn't true?

  • There is another error that occures when an using oneclik button. That is a message "ERROR CREATING CONTROL - OneClick1"

    anyone knows how to avoid it ?



    thanks in advanced,

Comments have been disabled for this content.