.

Double Submit Protection

The ease with which a user can inadvertently or deliberately submit a web form multiple times is a persistent problem with web applications and web forms.

Typically this problem arises when a user attempts to submit a form and the server does not immediately respond to the request. The delay can be due to network traffic, high load, or a particularly complex action that must be completed. In any event, it does not take long before the user begins to wonder if they actually hit the button and they click it again just to make sure.

If your application is not properly designed, this can lead to multiple identical records, or actions being processed more than once. Sometimes with undesirable side effects (like multiple billing).

There are a couple of solutions to this problem.

  1. Warn the user to avoid clicking the button more than once…. this is a complete cop out on the part of the programmers and is just inviting trouble.
  2. Disable the submit button when pressed (using AJAX). This sort of works, but if the server is slow and non-responsive, this is likely to fail too. This will also fail if the user has JS turned off, or if they reload or go back to the form and resubmit.
  3. Disable the button via javascript. This works reasonably well, but if the user uses the browser back button, they can press the submit button again, resulting in a double submit. Again, this will fail if JS is turned off

The reality is that all of these solutions have problems and don’t really address the underlying issue. The real problem is not that the user is submitting multiple copies of the same request, the problem is that the server is trying to act on all of them.

The double_submit_protection plugin for Rails

How it works.

  1. User submits a request
  2. If the request is successfully processed, it records the time and the contents of the params
  3. if a request with an identical params array is processed within a predefined interval it will reject the request or redirect to another page

The user can then do whatever they want and they won’t be able to send the identical request again until the time interval expires.

If they change the content of the form and resubmit, that request will work.

Installation

script/plugin install svn://sciwerks.com/home/sciwerks/public_svn/double_submit_protection/

Usage:

First, restart your server.

To protect your entire application from duplicate post requests (non-ajax) you can put the following code in your application controller.

  1. ## application.rb
  2.  
  3. double_submit_protection  :method=>:post,
  4.                            :xhr => false,
  5.                            :interval => 60,
  6.                            :flash => {:warning => ‘Double Submit Detected’},
  7.                            :redirect_to => {:action=>’index’}

Disclaimer and Tips

This is beta stuff, so don’t be surprised if it breaks things. In particular, it can block some repeated AJAX calls if you don’t configure it properly.

Note that since it uses a before filter to do it’s dirty work, you can limit the actions it applies to by passing an parameters like

:only=>['action'], or
:except=>['action']

You may need to adjust the timeout period to suit your needs. Longer timeouts will make your session object get pretty big.

If you have a datetime_select in your form, it may circumvent any timeout interval. If this control defaults to the current time, then the plugin will not be able to tell that it is the same because the ‘minutes’ field will be changing. It should work fine against multiple submit presses, but it will have a harder time with refresh-submit cases. Future versions may have the option to ignore datetimes in the params.

Auto disabling the submit button with KRJS

Benjamin over at RubyOnRailsBlog was recently wondering about how to painlessly disable a submit button to prevent the user from hitting it multiple times.

This requires some sort of javascript hackery to achieve, but the easiest way I’ve found to do this is with my trusty old KRJS pocketknife.

  1. # in application controller
  2.  
  3. def on_commit_click
  4.   render :update |page| do
  5.    page[‘commit’].disabled = true
  6.    page << "$(’commit’).form.submit();"
  7.   end
  8. end
  9.  
  10. # in the form
  11.  
  12. <%= submit_tag ‘Submit’, :id=>’commit’ %>

This will add this behavior for all submit buttons with the ‘commit’ id. The hardest part here is remembering to add the ‘commit’ id.

Requires: KRJS Plugin

multiple_auto_complete plugin

Rails’ auto_complete control can be a really handy way to allow users to select an object or string from a list of pre-existing values, however, one big limitation to its use is the fact that you can’t have more than one of them on a single page.

UPDATE: just to clarify this a bit. If you have more than one auto_complete that tries to access the same model/field combo, you get into trouble. You can have multiple auto_completes so long as they access different model/field combinations.

I tried a couple of hacks around this problem (some involving method_missing), and finally found a method that works pretty well and doesn’t mess up your controllers.

The approach I ended up using was to rewrite the ‘auto_complete_for’ and ‘text_field_with_auto_complete’ functions so that they ignore trailing numbers on a dom_id. Once you install the plugin (see below), you simply use it like this…

  1. # View file
  2. <%= text_field_with_auto_complete ‘object_1′, ‘name’ %>
  3. <%= text_field_with_auto_complete ‘object_2′, ‘name’ %>

  1. # controller file
  2. auto_complete_for :object, :name

All the auto_complete fields named /object[-_]d+/ will call the ‘auto_complete_for_object_name’ function in the controller now. The default response function will pull out the correct information and respond as usual.

When you submit the form, the values will be passed in the params hash as

  1. params = { ‘object_1′ => {‘name’=>’name1′}, ‘object_2′=>{‘name’=>’name2′ }}

Installation

script/plugin install svn://sciwerks.com/home/sciwerks/public_svn/multiple_auto_complete/