I generated a new rails 5 --api --database=postgresql app the other day and only created one scaffold (Hero). I'm wondering how the strong parameters work in rails as I am seeing some odd behavior:
Controller looks like:
def create
hero = Hero.new(hero_params)
if hero.save
render json: hero, status: :created, location: hero
else
render json: hero.errors, status: :unprocessable_entity
end
end
My hero_params look like this:
def hero_params
params.require(:hero).permit(:name)
end
So I would assume that the client is required to submit a hash containing a "hero" key and it's allowed to have a "name" subkey that is allowed to be mass assigned when this controller action is called.
Meaning, the JSON should look like this:
{
"hero": {
"name": "test"
}
}
All is well but here is where I am seeing strange behavior. When the user submits the exact JSON as above, the parameters come in as:
Parameters: {"hero"=>{"name"=>"test"}}
Now if the user submits just:
{ "name": "test" }
It still creates a new resource and the parameters come in as:
Parameters: {"name"=>"test", "hero"=>{"name"=>"test"}}
Why are there two sets of parameters, one with the actual submitted data and one in the format of a hero object as if it was anticipating the mass assignment?
How come the require(:hero) doesn't raise an error when that key is not submitted? I assume the answer to this is because of what is automatically creating that second hash ("hero"=>{"name"=>"test"}} from question 1.
Any information on what I am missing here would be greatly appreciated, as this is barebones rails behavior out-of-the-box.
This behaviour comes from ActionController::ParamsWrapper:
Wraps the parameters hash into a nested hash. This will allow clients to submit requests without having to specify any root elements.
Rails applications have parameter wrapping activated by default for JSON requests. You can disable it globally by editing config/initializers/wrap_parameters.rb, or for individual controllers by including wrap_parameters false in the controller.
Related
I work in a classic Rails project which contains its own API for mobile devices.
When I am sending a JSON, with all attributes a model owns to my API, for example a user object with its nested user profile attributes, it works fine.
The JSON looks like this:
{
"email": "user-1#example.com",
"user_profile_attributes": {
"display_name": "Awesome user",
"field_a": "string",
"field_b": "string"
}
}
When I now remove a field like field_a from my JSON request, I would expect that Rails backend will ignore the field when updating a database record, using the update method of ActiveRecord. Unfortunately ActiveRecord decides to nullify my missing field instead.
The JSON I am sending without field_a looks like that:
{
"email": "user-1#example.com",
"user_profile_attributes": {
"display_name": "Awesome user",
"field_b": "string"
}
}
The INSERT in my logfile shows me that the field is set to NULL and in my database the field is also set to NULL after update is called on my params.
What I would like to know is if that is the correct behaviour and if this behaves the same in a regular Rails API only projects. How could I prevent Rails or ActiveRecord to not write NULL to fields in the database which are not present in my posted JSON request to the API? Also how can I prevent Rails or ActiveRecord from deleting a nested relation object when its not part of my request JSON? For example if you delete the entire user_profile_attributes node, ActiveRecord will delete it.
My update method in my controller looks like this:
def update
respond_to do |format|
if #current_user.update(update_user_params)
format.json { render 'api/app/v1/users/show', status: :ok, locals: { user: #current_user } }
else
render json: #current_user.errors, status: :unprocessable_entity
end
end
end
def update_user_params
params.require(:user).permit(
:email,
:password,
user_profile_attributes: [:display_name, :field_a, :field_b]
)
end
For a better code demonstration I've written a sample project with Rails 6 which does the same what I do in my project. It includes also an openapi.yml for Pawn, Insomnia or Postman to test the projects API at /api/app/v1 easily.
The code for the update method I use is at that position on my users_controller.rb:
https://github.com/fuxx/update-db-question/blob/master/app/controllers/api/app/v1/users_controller.rb#L16
My sample project is located here on GitHub:
https://github.com/fuxx/update-db-question
Thanks in advance!
Try changing accepts_nested_attributes_for :user_profile to accepts_nested_attributes_for :user_profile, update_only: true in your User model.
I am trying to configure my ruby on rails application in such a manner that I can update values with http Patch calls from for example a Angular app. Currently I have the following method of which I expect it to work:
users_controller.rb
def safe_params
params.require(:id).permit(:email)
end
def update
user = User.find(params[:id])
user.update_attributes(safe_params)
render nothing: true, status: 204
end
However, I get the following error when I pass some simple JSON:
undefined method `permit' for "500":String
Passed JSON:
{"email":"newadres#live.com", "id":500}
Do you guys know what I am doing wrong?
I believe you are misunderstanding the purpose of require and permit.
require is generally used in combination with a Hash and a form, to make sure the controller receives an Hash that exists and contains some expected attributes. Note that require will either raise, or extract the value associated with the required key, and return that value.
permit works as a filter, it explicitly allows only certain fields. The returned value is the original params Hash, whitelisted.
In your case, require does not make any sense at all, unless you pass a nested JSON like this one
{"user": {"email":"newadres#live.com", "id":500}}
but even in that case, it would be
params.require(:user).permit(:email)
In your current scenario, the correct code is
params.permit(:email)
One way to fix this, keeping with the spirit of the Rails docs:
def safe_params
params.require(:user).permit(:email)
end
And update the json:
{"user": {"email":"newadres#live.com"}, "id": 500}
You should change the order between require and permit, like that
params.permit(:email).require(:id)
because permit returns the entire hash, while require returns the specific parameter
Reference here
UPDATE
However, as others pointed out, you shouldn't use require with a single attribute, as it is most commonly used for hashes instead
Is there any way to remove sensitive fields from the result set produced by the default ActiveRecord 'all', 'where', 'find', etc?
In a small project that I'm using to learn ruby I've a reference to User in every object, but for security reasons I don't want to expose the user's id. When I'm using a simple HTML response it is easy to remove the user_id simply by not using it. But for some task I'd like to return a json using something like:
def index
#my_objects = MyObject.all
respond_to do |format|
...
format.json { render json: #my_objects, ...}
...
end
end
How do I prevent user_id to be listed? Is there any way to create a helper that removes sensitive fields?
You can use the as_json to restrict the attributes serialized in the JSON response.
format.json { render json: #my_objects.as_json(only: [:id, :name]), ...}
If you want to make it the default, then simply override the method in the model itself
class MyObject
def serializable_hash(options = nil)
super((options || {}).merge(only: [:id, :name]))
end
end
Despite this approach is quick and effective, it rapidly becomes unmaintainable as soon as your app will become large enough to have several models and possibly different serialization for the same type of object.
That's why the best approach is to delegate the serialization to a serializer object. It's quite easy, but it will require some extra work to create the class.
The serializer is simply an object that returns an instance of a model, and returns a JSON-ready hash. There are several available libraries, or you can build your own.
So I observed some weird behaviour while implementing an endpoint for a RESTful API I am creating for a mobile client. I am using a PUT method to update an attribute on the User model. I send the user's id as a URL parameter and the value to update inside a JSON object. Everything seems to work just fine but when I check the parameters via the rails logs I noticed something strange. For some reason there is an extra parameter being sent to the backend that I can't seem to explain. Here are the logs I am seeing when I call this endpoint from the mobile client:
Parameters: {"enable_security"=>true, "id"=>"7d7fec98-afba-4ca9-a102-d5d71e13f6ce", "user"=>{}}
As can be seen above an additional "user"=>{} is appended to the list of parameter entries. I see this when I print out the params object as well. I can't seem to explain where this is coming from. I also checked the mobile client just to be safe and there is no where in code where I send a parameter with a key user. This is very puzzling to me and makes me think I am missing something fairly simple. Why is there an empty object with the user key being sent to the backend RESTful API?
Update to Provide More Information
Here is the code that gets called when the user hits the endpoint that updates the user User model:
#PUT /:id/user/update_security_settings
def update_security_settings
#user = User.find_by_id(params[:id])
#user.advanced_security_enabled = params[:enable_security]
respond_to do |format|
if #user.save
response = {:status => "200", :message => "User's security settings updated."}
format.json { render json: response, status: :ok }
else
format.json { render json: #user.errors, status: :unprocessable_entity }
end
end
end
Update in Response to User's Comments
Here are the routes that pertain to the user_controller, the view controller that defines all endpoints that deal with creating and updating the User model.
post '/user/upload_profile', to: 'user#upload_profile'
get '/:id/user', to: 'user#find_user'
put '/:id/user/update_security_settings', to: 'user#update_security_settings'
resources :user, :defaults => { :format => 'json' }
Does this comment really mirror your actual route?
#PUT /:id/user/update_security_settings
I'd expect it to be /user/:id/update_security_settings instead.
Can you show us your config/routes.rb - My wild guess is that your routes are somehow configured to expect an actual nested user param, which you don't send (of course) and therefor appears empty in the logs.
Update:
Some of your routes are unusual. You actually don't need the find_user route as it should be covered under resources :user as show action (provided you defined a show method in your controller, which is the default way to retrieve a single resource item; so no need for find_user)
For custom routes like your update_security_settings action I'd suggest to stick to the default pattern, like resource/:id/actionand nesting it in the default resourceful route. Putting the id before the resource is very unusual, confusing and may actually be related to your issue (thoguh I#m not sure about that). Try cleaning up your routes.rb liek this:
# notice that resources expects the plural form :users
resources :users do
member do
patch :update_security_settings
post :upload_profile
# any other custom routes
end
end
This will result in routes like GET /users (index), GET /users/1 (show) and PATCH /users/1/update_security_settings.
More on routing an be found here: Rails Guides | Routing From The Outside In
Please check if the changes above remove your empty user param.
Check your configuration in
config/initializers/wrap_parameters.rb
wrap_parameters format: [] this list should not contain json then it will wrap the parameters to the root of you controller for all the json request. Refer api docs
I have a rails 4 application that uses postgresql. I also have a backbone.js application that pushes JSON to the rails 4 app.
Here's my controller:
def create
#product = Product.new(ActiveSupport::JSON.decode product_params)
respond_to do |format|
if #product.save
format.json { render action: 'show', status: :created, location: #product }
else
format.json { render json: #product.errors, status: :unprocessable_entity }
end
end
end
def product_params
params.require(:product).permit(:title, :data)
end
I'm trying to parse the JSON and insert the product, but on insert, I'm getting the error:
TypeError (no implicit conversion of ActionController::Parameters into String):
Thanks for all help!
Your mileage may vary, but I fixed a smilar problem by a bandaid code like this:
hash = product_params
hash = JSON.parse(hash) if hash.is_a?(String)
#product = Product.new(hash)
The particular problem I had was that if I was just doing JSON.parse on the params that contained the object I wanted to create, I was getting this error while unit testing, but the code was working just fine when my web forms were submitting the data. Eventually, after losing 1 hour on logging all sorts of stupid things, I realized that my unit tests were somehow passing the request parameter in a "pure" form -- namely, the object was a Hash already, but when my webforms (or manual headless testing via cURL) did sumbit the data, the params were as you expect -- a string representation of a hash.
Using this small code snippet above is, of course, a bandaid, but it delivers.
Hope that helps.
Convert hash into JSON using to_json
The error is telling you that ActiveSupport::JSON.decode expects to be provided with a string, but is unable to coerce the argument you are providing it into a string. The argument provided to it here is "product_params" which returns a ActionController::Parameters (a loosely wrapped Hash).
If you are using "out of the box" style Backbone there is no need to decode what is being POSTed to that action. Just change the action to:
#product = Product.new(product_params)
The structure of your product_params method indicates that the action is expecting the data you are POSTing to look like this:
{
product: {
title: "Foo",
data: "bar"
}
}
and that your Product model has two attributes that will be populated by .new: title and data.
If you are explicitly encoding something into JSON on the client side you need to figure out what POST parameter it is being submitted as and the decode it on the server (again - there is almost certainly not a good reason to jump through hoops like that).