13

I'm looking for a generic way to prevent multiple form submissions. I found this approach which looks promising. Whereas I do not want to include this snippet in all of my views. Its probably easier to do this with a request processor or a middleware.

Any best practice recommendations?

3
  • You could create a base form class which all your forms inherit from which could perform the session check in a custom validation method so form.valid() would return false if the user has already entered data in the form. Commented Mar 27, 2013 at 23:17
  • 2
    I don't understand that gist at all. It isn't updating the session. Commented Mar 28, 2013 at 2:08
  • You are right. The GET part where the session has to be updated is not shown. But as he creates a hash-string based upon the csrf token, which has to be set in the GET request, it will probably work. Commented Mar 28, 2013 at 7:50

5 Answers 5

22

Client side, start with JavaScript. You can never trust the client, but its a start.

i.e.

onclick="this.disabled=true,this.form.submit();"

Server side you 'could' insert something into a database i.e. a checksum. If its a records you are insert into a database use model.objects.get_or_create() to force the uniqueness on database level you should use unique_together.

Lastly: HTTPRedirect is best, The the method I use when a user processes a payment is to just issue a HTTPRedirect() to the thank you/conformation page. This way refreshing the form won't resubmit and if they go back and try to submit the form again (without refreshing the form) the Django Cross Site Request Forgery (CSRF) will fail, perfect!

Sign up to request clarification or add additional context in comments.

6 Comments

Thanks for your reply. HTTPRedirect does not really help for this case, as the re-direct happens after the form has been validated in the post request. I want to prevent the user from submitting the same form twice (by clicking twice on the submit button within a short time frame - while thinking the form has not been submitted as server did not yet send a resp). Nevertheless its best practice to do a re-direct after a post submit, but for other reasons. unique_together does not help. think of a form to create tasks. a double click on form submit task leads to two tasks with the same values..
..which is ok since the user should be able to create tasks with for example the same name. No need for database uniqueness. The only valid approach for this case might be the javascript approach which I will give a try.
Finally I achieved multiple form submission using angular.js. See this question for details: stackoverflow.com/questions/15807471/…
Does HTTPRedirect and CSRF really work to avoid a user pressing the back button and post the form again? Because I think I am trying it but it does not work. See my post stackoverflow.com/questions/20412818/…
This is not quite working as when you disable the submit button browser just cancels the form submittion
|
4

I also try to find a good way to prevent double records generation when a user dbl-click on a submit button. It's not about the PRG issue that is easily fixed by redirection.

So, regards on this basic concern, the solution with HTTPRedirect on server-side doesn't help.

On client-side, I found two problems when I disable the button before submit:

  1. With HTML5 validation, the form.submit() will be interupted by browser if the form is invalid => submit button is still disabled=true.
  2. When the user submit the form and do a back in browser history, the DOM will be loaded from browser's cache => submit button is still disabled=true.

So here is my workaround for the first client-side problem (HTML5 validation):

isFormHtml5Valid(form) {
  for(var el of form.querySelectorAll('input,textarea,select')){
    if(!el.checkValidity())
      return false;
  }
  return true;
}

mySubmitButton.onclick = function() {
  if(this.form && isFormHtml5Valid(this.form))
    this.disabled=true;
  this.form.submit();
}

I try to find a client-side workaround for the second client-side problem (browser cache the DOM) but nothing worked (onbeforeunload, ...). So the workaround I currently use for "browser cache" issue is add a @never_cache decoration on the top of concerned views (from server-side, indicate to client-side to not caching). Please let me know if you have a better workaround.

Last but not least, I would really appreciate to fix this issue on server side. The CSRF solution seems not suitable since a CSRF token is generated by session (not for each form). So here is the status of my work and my question:

  • Fix this issue on client-side is OK but doesn't look like a good solution to me. How could we avoid to validate this multiple form submition on server-side?

Let me know if you have a good solution for that.


Edit 1: May be a small part of the answer: Synchronizer (or Déjà vu) Token

But I didn't find any implemantation of that in Django.

2 Comments

I think this should be marked as the best answer for this question ! On client side I do also other thing, which is showing overlay div element over submit button or whole page, which eats all clicks and prevents submit button being clicked again
For the serverside, how about writing middleware that hashes the POST data and caches it for a short time?
1

Use HttpResponseRedirect

create a new view(lets say thank_you) for successful message to display after form submission and return a template.

After successful form submission do return HttpResponseRedirect("/thank-you/") to the new thank-you view

from django.http import HttpResponseRedirect

def thank_you(request, template_name='thank-you.html'):
    return render_to_response(template_name,locals(),context_instance=RequestContext(request))

and in urls.py

url(r'^thank-you/$','thank_you', name="thank_you")

Multiple form submission happens because when page refreshes that same url hits, which call that same view again and again and hence multiple entries saved in database. To prevent this, we are required to redirect the response to the new url/view, so that next time page refreshes it will hit that new url/view.

Comments

1
  1. After a successful submission respond with a redirect so a reload doesn't cause another submission
  2. Disable the submit button on form submission

As for #2, it's best to do that in the onsubmit handler and not the onclick for the submit button. This will handle cases such as multiple submit buttons or if there's any HTML5 client-side form validation. In jQuery it'd look something like:

$('#edit_form').submit( function(event) {
    // disable to avoid double submission
    $('#submit_button').attr('disabled', true);
});

However, one issue this doesn't account for is if the user cancels the submit part way. For example, if you click "submit" and then immediately hit "Esc", the browser will stop the submission but "submit" button will already be disabled.

Also, it's possible to have a form that is submitted by hitting "enter" and this solution wouldn't prevent that form from submitting twice.

Comments

0

I have been longing for a proper analysis and solution for this issue for over a decade. After searching a bit around online, I decided to try a method that should be able to prevent duplicate submissions by hashing the POST in a Django class-based view (possible to translate this to a function-based view, too).

class MyCreateView(CreateView):

    def post(self, request, *args, **kwargs):

        # Unique key to store session for this view
        self.session_form_hash = f"form-submission+{self.__class__.__name__}"
        # Unique key for storing the pk when the object is created
        self.session_last_saved_pk = session_form_hash + "-pk"

        # Calculate hash of the POST data
        excluded = {
            "csrfmiddlewaretoken",
        }
        self.post_hash = hash(
            tuple(
                sorted(
                    (k, v) for k, v in self.request.POST.items() if k not in excluded
                )
            )
        )

        # Previously calculated hash
        previous_post_hash = self.request.session.get(self.session_form_hash)

        # Form has already been processed!
        if self.post_hash == previous_post_hash:
            # Fetch the previous object and return a helpful message to the user
            self.object = get_object_or_404(
                MyModel,
                pk=self.request.session[self.session_last_saved_pk]
            )
            messages.warning(
                self.request, "This form was submitted several times. It hans been processed just once."
            )
            return HttpResponseRedirect(self.get_success_url())

        return super().post(request, *args, **kwargs)

    def form_valid(self, form):
            self.object = form.save()
            # Now that the form is valid and saved, we're gonna update our session
            self.request.session[self.session_form_hash] = self.post_hash
            self.request.session[self.session_last_saved_pk] = self.object.pk
            return HttpResponseRedirect(self.get_success_url())

As the OP asks for "best practice", I would have to say: I don't think that it's best practice to use javascript to disable the submit button. That causes all sorts of new issues.

The idea to redirect to a "thank you" page is fine, but this should already be standard practice and doesn't fix issues caused by network problems, double-clicks etc.

You can read more about it on my blog from today 😇️ https://overtag.dk/v2/blog/duplicate-form-submissions-and-how-to-handle-them-in-django/

2 Comments

This won't solve problems like double clicks, etc. as it would be prone to race conditions. The only way this would work would be if you have only one worker (process or thread). When multiple requests happen concurrently it is possible that by the time the session is updated the other request has already begun and read the stale values from the session.
@AbdulAzizBarkat - this was an initial attempt with more details and considerations in the blog post. If you are concerned about high concurrency (rightfully so), you can use a cache instead of Django's session store 👍 You can add the session ID to the cache key.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.