loading...

Passing Rails controller params to Sidekiq

prathamesh profile image Prathamesh Sonpatki Originally published at prathamesh.tech on ・3 min read

If we pass Rails controller params, directly to a Sidekiq worker then they are not parsed correctly by Sidekiq when the job is executed. Let's see an example.

class UsersController < ApplicationController
  def create
    CreateUserWorker.perform_async(user_params)
  end

  private

    def user_params
      params.require(:user).permit(:name, :email)
    end
end

class CreateUserWorker
  include Sidekiq::Worker

  def perform(params)
    User.create!(params)
  end
end

This is because Sidekiq expects the arguments to be primitive types such as Hash, String, Boolean, Array etc.

Sidekiq uses JSON.generate to generate JSON and then pushes the job data to Redis. When the job is pulled from Redis for execution, Sidekiq parses the saved job data using JSON.parse and then passes it to the worker.

Let's see what is the output of JSON.generate with controller params as argument.

JSON.generate user_params
=> "\"{\\\"user\\\"=>{\\\"name\\\"=>\\\"Prathamesh\\\", \\\"email\\\"=>\\\"prathamesh@exampless.com\\\"}}\""

This is not the expected output because when Sidekiq will parse it, we will not get the ActionController::Parameters back.

job_data = JSON.generate user_params

JSON.parse job_data
=> "{\"user\"=>{\"name\"=>\"Prathamesh\", \"email\"=>\"prathamesh@exampless.com\"}}"

We can see that the data is not parsed correctly, because the JSON payload was not generated correctly.

Ruby's JSON.generate method treats certain primitive objects such as Hash, String, True, False as special cases when generating JSON representation. Whereas for custom objects, it checks whether the object responds to to_json or not. If yes then it returns the output of calling to_json on that object. If the object does not respond to the to_json method, then it generates its JSON representation considering the object as String. This code can be found here.

In case of ActionController::Parameters objects, Ruby generates JSON representation considering it as String. Even though ActionController::Parameters gets to_json method from Active Support, still Ruby is not able to figure out that ActionController::Parameters objects respond to to_json.

# https://github.com/ruby/ruby/blob/8e517942656f095af2b3417f0df85ae0b216002a/ext/json/generator/generator.c#L1018

else if (rb_respond_to(obj, i_to_json)) {
        tmp = rb_funcall(obj, i_to_json, 1, Vstate);
        Check_Type(tmp, T_STRING);
        fbuffer_append_str(buffer, tmp);
    }

As ActionController::Parameters respond to to_json it should go into this branch of code but somehow it goes into the final else code branch.

# https://github.com/ruby/ruby/blob/8e517942656f095af2b3417f0df85ae0b216002a/ext/json/generator/generator.c#L1022-L1026

else {
        tmp = rb_funcall(obj, i_to_s, 0);
        Check_Type(tmp, T_STRING);
        generate_json_string(buffer, Vstate, state, tmp);
    }

I was not able to figure out why that is happening.

So to fix this, we need to pass the params as Hash object to the Sidekiq job. A simple way will be to pass the params after calling to_h on them.

class UsersController < ApplicationController
  def create
    CreateUserWorker.perform_async(user_params.to_h)
  end

  private

    def user_params
      params.require(:user).permit(:name, :email)
    end
end

Importantly, to_h will result into an error when we have not permitted any of the parameters. You can also call to_unsafe_h to pass all the parameters without permitting.

Before Rails 5, ActionController::Parameters was inheriting from Hash class so the JSON serialization and deserialization was working properly without converting the params to a Hash. But now because ActionController::Parameters do not inherit from Hash anymore, we need to pass it to Sidekiq worker as a Hash.

Posted on Apr 27 by:

prathamesh profile

Prathamesh Sonpatki

@prathamesh

Prathamesh Sonpatki is consultant experienced in Ruby, Ruby on Rails.

Discussion

markdown guide