How to Prevent Submitting Duplicate Forms PT. II
Yesterday I published a post and gist on how to prevent users from submitting duplicate form data in your Rails app. I created a simple method that prevents client browsers from caching the form, and I run it via an after_filter in the controller. In this post, I’ll show you an extra layer of protection, that can be used in conjunction with the aforementioned approach. Now that we prevent the user from entering duplicate data in a form by addressing the GET request, let’s fill the remaining holes by preventing duplicate data from being saved to the database on POST request.
ActiveRecord offers a variety of useful tools to Rails developers out-of-the-box, practically everything you need from your ORM. One of my favorite methods is first_or_initialize (first_or_initialize_by in Rails 4.0.2+). This method returns the first record in a relation if it exists. If the relation returns an empty array, then it instantiates a new instance of that class with any optional attributes it is passed. Example:
class PuppiesController < ApplicationController before_filter :load_puppy, only: [ :new, :create, :show, :update, :edit, :delete ] def new ## If user already has a record in the puppies table ## created this year ## then redirect them to edit that Puppy. if [email protected]_record? flash[ :notice ] = "You may only create one puppy per year. You may edit your existing puppy." redirect_to edit_puppy_path( @puppy ) and return end end def create ## prevent malicious user from setting the user_id @puppy.user_id = current_user.id if @puppy.errors.empty? && @puppy.save redirect_to @puppy else ## Tell user they need to fix their mistakes render :edit end end private def load_puppy @puppy = Puppy. includes( :user ). by_user_from_this_year( current_user.id ). first_or_initialize( params[ :puppy ] ) end end
Once again, I do think we should use the first_or_initialize approach in conjunction with our preventing forms from being cached by the browser. And of course, this is only one layer of protection, it’s always wise to have other layers at the model and database level. Gotta protect that data.
Using :first_or_initialize will allow us to update a user’s existing record if she indeed has one, instead of creating a duplicate. However, from a UX perspective, you need to make it clear to your users what is happening when they press Back, and then they edit a form and re-submit it. If the form is cached and they press back, they may assume that they’re creating a totally new record, instead of updating the existing record they just created on the first submission. This can get messy. So, I like to use both approach-1 and approach-2 together; approach-1 to prevent the browser from caching form data (fixes 99.9% of cases) and approach-2 to prevent users from saving duplicate records via the POST request with first_or_initialize (the other 0.1%). Forcing the browser to hit the server when the user presses back also gives us the opportunity to warn the user that they have an existing record, and that they’re being redirected to an edit page. (And we like the server side, because we can use Ruby there!)
All together simply:
class ApplicationController < ActionController::Base prepend_before_filter :authenticate_user! private ## Tell client not to cache the response def client_will_not_cache_response response.headers[ "Cache-Control” ] = “no-cache, no-store” response.headers[ “Pragma” ] = “no-cache” response.headers[ “Expires” ] = “Fri, 01 Jan 1990 00:00:00 GMT” end end class PuppiesController < ApplicationController before_filter :load_puppy, only: [ :new, :create, :show, :update, :edit, :delete ] after_filter :browser_will_not_cache_response, only: :new def new ## If user already has a record in the puppies table ## created this year ## then redirect them to edit that Puppy. if [email protected]_record? flash[ :notice ] = "You may only create one puppy per year. You may edit your existing puppy." redirect_to edit_puppy_path( @puppy ) and return end end def create ## prevent malicious user from setting the user_id @puppy.user_id = current_user.id if @puppy.errors.empty? && @puppy.save redirect_to @puppy else ## Tell user they need to fix their mistakes render :edit end end private def load_puppy @puppy = Puppy. includes( :user ). by_user_from_this_year( current_user.id ). first_or_initialize( params[ :puppy ] ) end end
Some final notes about this code...I added the :load_puppy method to DRY up some common code, refactoring it into a single method called prior to our controller actions. The authenticate_user! method you see in the Application controller is a Devise method for authorizing users--it's there for reference since so many of us use Devise. I also moved the :redirect_if_existing before_filter method--you may remember it from my previous post--into the :new action definition for the sake of clarity. It was only being used in that one method all.
That’s it.

















