Rails converts empty arrays into nils in params of the request - ruby-on-rails

I have a Backbone model in my app which is not a typical flat object, it's a large nested object and we store the nested parts in TEXT columns in a MySQL database.
I wanted to handle the JSON encoding/decoding in Rails API so that from outside it looks like you can POST/GET this one large nested JSON object even if parts of it are stored as stringified JSON text.
However, I ran into an issue where Rails magically converts empty arrays to nil values. For example, if I POST this:
{
name: "foo",
surname: "bar",
nested_json: {
complicated: []
}
}
My Rails controller sees this:
{
:name => "foo",
:surname => "bar",
:nested_json => {
:complicated => nil
}
}
And so my JSON data has been altered..
Has anyone run into this issue before? Why would Rails be modifying my POST data?
UPDATE
Here is where they do it:
https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/http/request.rb#L288
And here is ~why they do it:
https://github.com/rails/rails/pull/8862
So now the question is, how to best deal with this in my nested JSON API situation?

After much searching, I discovered that you starting in Rails 4.1 you can skip the deep_munge "feature" completely using
config.action_dispatch.perform_deep_munge = false
I could not find any documentation, but you can view the introduction of this option here:
https://github.com/rails/rails/commit/e8572cf2f94872d81e7145da31d55c6e1b074247
There is a possible security risk in doing so, documented here: https://groups.google.com/forum/#!topic/rubyonrails-security/t1WFuuQyavI

Looks like this is a known, recently introduced issue: https://github.com/rails/rails/issues/8832
If you know where the empty array will be you could always params[:...][:...] ||= [] in a before filter.
Alternatively you could modify your BackBone model's to JSON method, explicitly stringifying the nested_json value using JSON.stringify() before it gets posted and manually parsing it back out using JSON.parse in a before_filter.
Ugly, but it'll work.

You can re-parse the parameters on your own, like this:
class ApiController
before_filter :fix_json_params # Rails 4 or earlier
# before_action :fix_json_params # Rails 5
[...]
protected
def fix_json_params
if request.content_type == "application/json"
#reparsed_params = JSON.parse(request.body.string).with_indifferent_access
end
end
private
def params
#reparsed_params || super
end
end
This works by looking for requests with a JSON content-type, re-parsing the request body, and then intercepting the params method to return the re-parsed parameters if they exist.

I ran into similar issue.
Fixed it by sending empty string as part of the array.
So ideally your params should like
{
name: "foo",
surname: "bar",
nested_json: {
complicated: [""]
}
}
So instead of sending empty array I always pass ("") in my request to bypass the deep munging process.

Here's (I believe) a reasonable solution that does not involve re-parsing the raw request body. This might not work if your client is POSTing form data but in my case I'm POSTing JSON.
in application_controller.rb:
# replace nil child params with empty list so updates occur correctly
def fix_empty_child_params resource, attrs
attrs.each do |attr|
params[resource][attr] = [] if params[resource].include? attr and params[resource][attr].nil?
end
end
Then in your controller....
before_action :fix_empty_child_params, only: [:update]
def fix_empty_child_params
super :user, [:child_ids, :foobar_ids]
end
I ran into this and in my situation, if a POSTed resource contains either child_ids: [] or child_ids: nil I want that update to mean "remove all children." If the client intends not to update the child_ids list then it should not be sent in the POST body, in which case params[:resource].include? attr will be false and the request params will be unaltered.

I ran into a similar issue and found out that passing an array with an empty string would be processed correctly by Rails, as mentioned above.
If you encounter this while submitting a form, you might want to include an empty hidden field that matches the array param :
<input type="hidden" name="model[attribute_ids][]"/>
When the actual param is empty the controller will always see an array with an empty string, thus keeping the submission stateless.

Related

Unexpected token when sending nested params to worker - Rails 6

I have a Sidekiq worker to which I send my controller params. My controller params look like this.
def my_params
params.require(:users).permit(employees: [:param1, param_requested_attributes: [:attribute]])
end
So when I send my JSON to the controller and check with byebug, params are correctly formatted, but when I send them to the worker like:
MyWorker.perform_async(my_params)
I iterate over each "employee" as:
my_params.each do |employee|
data = JSON.parse(raw_data.gsub('=>', ':')) # to correctly format my json data
end
and I get an "unexpected token error" because 'params_requested_attributes' looks like:
"params_requested_attributes"=>[<ActionController::Parameters> {"attribute"=>"value"} permitted: true> ]
My question is, how can I avoid this "ActionController::parameters" when trying to JSON.parse my params? It only happens when I try to use these nested_attributes. I just want a raw json, but for some reason I get this "action controller params".
Your problem is that my_params:
def my_params
params.require(:users).permit(...)
end
gives you an instance of ActionController::Parameters and the params_requested_attributes inside that is another instance of ActionController::Parameters. Then you enqueue a job with:
MyWorker.perform_async(my_params)
which has to serialize my_params into a database and that serialization isn't handling the inner ActionController::Parameters the way you want it to.
You could convert the parameters to a nested hash yourself:
MyWorker.perform_async(my_params.to_h)
to get rid of the the ActionController::Parameters and end up with plain hashes and arrays in your job.
If you really want JSON that your job manually unpacks then you'll have to massage my_params a little more. Perhaps you'd want to my_params.to_h and then JSON encode then nested params_requested_attributes. This sounds like a solution in search of a problem, I think passing my_params.to_h to the job is where you want to go.
JSON.parse turns a json string into a ruby object. So if you need just the raw json string you should use raw_data and skip the .gsub call.
However I cannot see from your example how raw_data relates to the employee value.

Rails: "except" vs "slice" to take out certain parameter?

I am trying to create a class instance with one particular key extracted out and save for later
consider this is the type of param:
Parameters: {"p1": "foo", "p2": "bar", "p3":"param that i want to extract" >
I would do something like:
#my_class = MyClass.new(whiteList_function.except(:p3))
or something like
#my_class = MyClass.new(whiteList_function)
#my_class_key = #my_class.slice(:p3)
My whiteList_function:
def whiteList_function
params.require(:my_class).permit(:p1,:p2,p3)
end
My concern is that, I have searched on SO and some have mentioned the security risk of using except.
So my question is, in this particular usage(take out certain key and use them for later), should I use except or slice?
Using except with parameter hashes is dangerous because you're saying "allow any parameter other than this one" so it opens your model up to mass-assignment attacks.
Unless I'm not thinking of another attack vector you should be fine, because you're filtering your acceptable parameter hash with strong_parameters in your whiteList_function. So, you're fine further filtering that hash with either slice or except, whichever is more appealing to you.
I'm no Rails user and maybe I misunderstand your question but in plain Ruby here is what I would do
class MyClass
attr_reader :params
def initialize params
#params = whiteList_function params
end
def whiteList_function params
whiteList = [:p1, :p2]
params.select{|key| whiteList.include? key }
end
end
MyClass.new({"p1": "foo", "p2": "bar", "p3":"param that i want to extract"})
# #<MyClass:0x00000002802a58 #params={:p1=>"foo", :p2=>"bar"}>

Updating a boolean field in Rails through a JSON API?

I have a Settings table with 5 boolean fields. I want to be able to create and update those fields via JSON. From what I understand, Rails converts all parameters to strings, so that boolean values always return true. If I send:
{ "settings": { "setting1":true, "setting2":false } }
And then try doing: Setting.new(params[:settings]), both settings will be true in the database, since the second setting's value of false is translated to "false", and thus actually evaluates to true. Actually I can't even do that, as I get:
NoMethodError (undefined method `stringify_keys' for #<String:0x000000213dcbd0>)
on that line. Some suggestions from the internet say to compare the parameters against "true", and then store that. This is a huge pain though, because then I can't take advantage of mass-assignment. I don't want to have to do this:
#setting = Setting.new
#setting.setting1 = (params[:settings][:setting1].eql? "true")
...
For all 5 fields, especially since I will have to do that for create, update, and even from other controllers (some controllers accept JSON to create a settings object along with their own attributes).
Is there a better way to go about this? JSON APIs seem pretty standard with Rails, it seems like this would be taken care of in a more elegant way?
You can pass "0" instead of false to set the value of the field to false.
How about just making a convertor class and sticking it in a before filter?
class ApplicationController < ActionController::Base
before_filter BooleanFilter
end
class BooleanFilter
def self.filter(controller)
# change all relevant string params to booleans, raise an exception if something other than "true" or "false" is detected
# eg. params[:setting][:setting1] = ( params[:setting][:setting1] == "true" ? true : false )
end
end
It is probably more consistent with Rails to assign the changed params to a new Hash object and to pass that to the model.

Getting the rails 'params' without the defaults?

Is there a neat way in rails to get a hash of the params without the default ones of 'action' and 'controller'? Essentially without any param that wasn't added by me.
I've settled for:
parm = params.clone
parm.delete('action')
parm.delete('controller');
But wondering if there is a neater way to do this?
You could use except:
params.except(:action, :controller)
http://as.rubyonrails.org/classes/ActiveSupport/CoreExtensions/Hash/Except.html
request.path_parameters
returns path_parameters
request.query_parameters
returns request_parameters
You are looking for the latter.
If you are working in a controller, you should also have access to the request object.
To make a long story short, rails and rack groom incoming GET/POST requests (form, xml, json) and pull out the parameters so that developers have a consistent way of accessing them.
ActionDispatch exposes the consolidated list of params via:
# ActionPack 3.1.8 - action_dispatch/http/parameters.rb
# Returns both GET and POST \parameters in a single hash.
def parameters
#env["action_dispatch.request.parameters"] ||= begin
params = request_parameters.merge(query_parameters)
params.merge!(path_parameters)
encode_params(params).with_indifferent_access
end
end
alias :params :parameters
As you can see, params is an alias for the parameters method which is a merged hash of two sub-hashes: request_parameters and path_parameters.
In your case, you don't want the path_parameters. Rather than using except, which forces you to know which path parameters you want to exclude, you can access your data via: request.request_parameters.
A word of caution: You may be better off using :except if you require the hash to be encoded and keys to be accessed as either strings or symbols. The last line of the parameters method handles that for you:
encode_params(params).with_indifferent_access
An alternative approach using except and ensuring that you are removing all rails non-request parameters:
path_params = request.path_parameters
params.except(*path_params.keys)
use
request.request_parameters
it excludes the path_parameters (controller and action)
I use
request.request_parameters.except(controller_name.singularize)
This strips out the nested object that is named after the active controller. For example with the following controller:
Class SessionController > ActionController::Base
def create
User.find_by(params[:email]).login(password: params[:password])
puts request.request_parameters
end
end
With the following posted value from a web form:
{email: 'test#example.com', password: 'password123'}
The console output will be:
{"email"=>"test#example.com", "password"=>"password123", "session"=>{"email"=>"test#example.com", "password"=>"password123"}}
The above lines of code avoid this.

Rails: modify form parameters before modifying the database

I'm working on a Rails app that sends data through a form. I want to modify some of the "parameters" of the form after the form sends, but before it is processed.
What I have right now
{"commit"=>"Create",
"authenticity_token"=>"0000000000000000000000000"
"page"=>{
"body"=>"TEST",
"link_attributes"=>[
{"action"=>"Foo"},
{"action"=>"Bar"},
{"action"=>"Test"},
{"action"=>"Blah"}
]
}
}
What I want
{"commit"=>"Create",
"authenticity_token"=>"0000000000000000000000000"
"page"=>{
"body"=>"TEST",
"link_attributes"=>[
{"action"=>"Foo",
"source_id"=>1},
{"action"=>"Bar",
"source_id"=>1},
{"action"=>"Test",
"source_id"=>1},
{"action"=>"Blah",
"source_id"=>1},
]
}
}
Is this feasible? Basically, I'm trying to submit two types of data at once ("page" and "link"), and assign the "source_id" of the "links" to the "id" of the "page."
Before it's submitted to the database you can write code in the controller that will take the parameters and append different information before saving. For example:
FooController < ApplicationController
def update
params[:page] ||= {}
params[:page][:link_attributes] ||= []
params[:page][:link_attriubtes].each { |h| h[:source_id] ||= '1' }
Page.create(params[:page])
end
end
Edit params before you use strong params
Ok, so (reviving this old question) I had a lot of trouble with this, I wanted to modify a param before it reached the model (and keep strong params). I finally figured it out, here's the basics:
def update
sanitize_my_stuff
#my_thing.update(my_things_params)
end
private
def sanitize_my_stuff
params[:my_thing][:my_nested_attributes][:foo] = "hello"
end
def my_things_params
params.permit(etc etc)
end
You should also probably look at callbacks, specifically before_validate (if you're using validations), before_save, or before_create.
It's hard to give you a specific example of how to use them without knowing how you're saving the data, but it would probably look very similar to the example that Gaius gave.

Resources