Passing Rails controller params to Sidekiq
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.
I am not able to figure out why that is happening. Rohit Kumar pointed in comments on this post that when you call JSON.generate
, the to_json
method is not called on the instance of ActionController::Parameters
which is the reason why Ruby does not use the to_json
output defined by Active Support for serialization. I have not fully understood yet what does rb_funcall(obj, i_to_json, 1, Vstate);
outputs in this case.
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 callto_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
.