ruby on fails

Jared Carroll

So recently, I had a Rails app that raised the following exception: ActiveRecord::MultiparameterAssignmentErrors. This was raised in ActiveRecord::Base#initialize. It was caused by a model that had a date attribute. I was using ActionView::Helpers::DateHelper#dateselect to get the value for a Job model’s `expireson` attribute. Apparently a user had selected an invalid date, e.g. June 31, 2007.

ActiveRecord::Base#initialize basically executes the following code:

Date.new 2007, 6, 31

Now, Ruby’s Date class will raise an ArgumentError which Rails puts in an ActiveRecord::AttributeAssignmentError object that is available via an array from ActiveRecord::MultiparameterAssignmentErrors#errors.

So I thought, How can I handle this nicely?

How about overriding the setter in my model?

class Job < ActiveRecord::Base

  def expires_on=(expires_on_date) # expires_on_date is a Ruby Date object already
  end

end

Nope. Rails is going to pass a Ruby Date object to Job#expires_on=. The ArgumentError exception would have already occurred.

How about overriding ActiveRecord::Base#initialize on Job?

class Job < ActiveRecord::Base

  def initialize(attributes)
    unless valid_date?(attributes['expires_on(1i)'],
                       attributes['expires_on(2i)'],
                       attributes['expires_on(3i)'])
      errors.add :expires_on, 'is invalid'
      attributes.delete 'expires_on(1i)'
      attributes.delete 'expires_on(2i)'
      attributes.delete 'expires_on(3i)'
    end
    super
  end

  def valid_date?(year, month, date)
    Date.new year, month, date
  rescue ArgumentError
    nil
  end

end

Nope. Too bad adding errors to ActiveRecord::Base#errors outside any of the validation callbacks, i.e. #validate, #validate_on_create, #validate_on_update doesn’t prevent the object from being saved. In other words, ActiveRecord::Base#errors is cleared before calling #validate. I’m glad of this behavior, because the code above sucks anyway. Overriding ActiveRecord::Base#initialize is crazy.

What if we just set expires_on to nil? Then if we don’t #validate_presence_of :expires_on, the object will be saved with a null expires_on date, even though the user thought they just entered an invalid date.

So if I can’t put it in the model nicely, I’ll have to rescue ActiveRecord::MultiparameterAssignmentErrors in the action in the controller, like so:

class JobsController < ApplicationController

  def create
    @job = Job.new params[:job]
    # save the job
  rescue ActiveRecord::MultiparameterAssignmentErrors
    @job.errors.add :expires_on, 'is invalid'
    render :action => :new
  end

end

Nope. Since ActiveRecord::MultiparameterAssignmentErrors is raised in ActiveRecord::Base#initialize, the job object is never created. Therefore, the job instance variable is nil and this code will raise a NoMethodError because NilClass doesn’t understand #errors.

How about:

class JobsController < ApplicationController

  def create
    @job = Job.new params[:job]
    # save the job
  rescue ActiveRecord::MultiparameterAssignmentErrors
    flash[:notice] = 'Expiration date is invalid'
    render :action => :new
  end

end

Nope. Since a job object is never created, when rendering JobsController#new you’ll get a bunch of errors because the job instance variable is nil.

So we ended up replacing the year, month and day dropdowns created by ActionView::Helpers::DateHelper#date_select with a Javascript calendar.

About thoughtbot

We've been helping engineering teams deliver exceptional products for over 20 years. Our designers, developers, and product managers work closely with teams to solve your toughest software challenges through collaborative design and development. Learn more about us.