Structuring Multipart FormData with Rails Naming Conventions

As part of my studies at https://flatironschool.com/, I encountered several caveats when attempting to utilize multipart FormData with a Rails API backend. My use case involved images with accompanying descriptions and tags.

Strong Params

When using strong params in Rails, we permit.require(), with being the singular name of the controller that we are in.

When POSTing to a Rails API endpoint, a hash of automagically added to the root of the request by Rails’ router.

For example, consider the following route in as well as a corresponding fetch:

post '/users', to: 'users#create'

If we log params from within the users#create action, we find the body of our fetch automagically nested inside of a hash:

# In UsersController
def create
puts params
end

# Server log, upon receiving the fetch and routing to user#create:
=> {"name"=>"Bryan", "controller"=>"users", "action"=>"create", "user"=>{"name"=>"Bryan""}}

To utilize strong params and mass assignment, we require that hash and permit its desired attributes.

def create
user = User.create(user_params)
render json: user
end
privatedef user_params
params.require(:user).permit(:name)
end

Strong Presumptions

Having always used similarly constructed fetches, I expected similar behavior from multipart FormData.

Let’s build a FormData, POST it and see what happens:

# In UserController
def create
puts params
puts user_params
end
privatedef user_params
params.require(:user).permit(:file, :name)
end
# Server log:Error occurred while parsing request parameters.Contents:------WebKitFormBoundaryFwYm7IV8PzeE6F6EContent-Disposition: form-data; name="file"; filename="CookieMonster.jpg"Content-Type: image/jpeg????JFIF??Compressed by jpeg-recompress???# Followed by pages of scrambled text...

As it turns out, we aren’t transmitting JSON — we need to remove the header from our fetch request. Let’s try again.

# Server log:=> <ActionController::Parameters {"file"=>#<ActionDispatch::Http::UploadedFile:0x00007fdca8487c40 @tempfile=#<Tempfile:/var/folders/hk/sgkbgpq57_37rq3l1gfjsb940000gn/T/RackMultipart20200909-42995-13a9put.jpg>, @original_filename="CookieMonster.jpg", @content_type="image/jpeg", @headers="Content-Disposition: form-data; name=\"file\"; filename=\"CookieMonster.jpg\"\r\nContent-Type: image/jpeg\r\n">, "name"=>"Bryan", "controller"=>"users", "action"=>"create"} permitted: false>ActionController::ParameterMissing: param is missing or the value is empty: user

Our file has transmitted, as has our name attribute, but is refusing our request. What happened to our root level hash? It turns out Rails will not generate the root level hash without the JSON header — we have to build it into the FormData structure ourselves. For this, we must use the Rails parameter naming conventions: https://guides.rubyonrails.org/v3.2.13/form_helpers.html#understanding-parameter-naming-conventions

A final attempt:

# Server log:#params
=> <ActionController::Parameters {"user"=>{"file"=>#<ActionDispatch::Http::UploadedFile:0x00007fe628706e60 @tempfile=#<Tempfile:/var/folders/hk/sgkbgpq57_37rq3l1gfjsb940000gn/T/RackMultipart20200909-43265-1yopi5p.jpg>, @original_filename="CookieMonster.jpg", @content_type="image/jpeg", @headers="Content-Disposition: form-data; name=\"user[file]\"; filename=\"CookieMonster.jpg\"\r\nContent-Type: image/jpeg\r\n">, "name"=>"Bryan"}, "controller"=>"users", "action"=>"create"} permitted: false>
#user_params
<ActionController::Parameters {"file"=>#<ActionDispatch::Http::UploadedFile:0x00007fe628706e60 @tempfile=#<Tempfile:/var/folders/hk/sgkbgpq57_37rq3l1gfjsb940000gn/T/RackMultipart20200909-43265-1yopi5p.jpg>, @original_filename="CookieMonster.jpg", @content_type="image/jpeg", @headers="Content-Disposition: form-data; name=\"user[file]\"; filename=\"CookieMonster.jpg\"\r\nContent-Type: image/jpeg\r\n">, "name"=>"Bryan"} permitted: true>

Success! Our file and attribute has transmitted, we have a hash and is happy.

Arrays

FormData is able to store arrays of values. To do so, we iteratively attach an item with the same name.

Let’s update our permitted params to allow an array of , and attempt a POST.

# In UserController
def create
puts params
puts user_params
end
privatedef user_params
params.require(:user).permit(tag_ids: [])
end
# Server log:#params
=> <ActionController::Parameters {"user"=>{"tag_ids"=>"5"}, "controller"=>"users", "action"=>"create"} permitted: false>
#user_params
<ActionController::Parameters {} permitted: true>

That didn’t work. Despite our JS console log showing the array, Rails only kept the last tag_id and made it a key/value pair. Because was expecting an array, it rejected the key/value pair.

The solution, again, is to use Rails parameter naming conventions while constructing the FormData. MDN notes this technique as “being compatible with PHP naming conventions”: https://developer.mozilla.org/en-US/docs/Web/API/FormData/append

# Server log:#params
=> <ActionController::Parameters {"user"=>{"tag_ids"=>["2", "3", "5"]}, "controller"=>"user", "action"=>"create"} permitted: false>
#user_params
<ActionController::Parameters {"tag_ids"=>["2", "3", "5"]} permitted: true>

Success!

Empty Arrays with Strong Params

Mass assignment with an empty array is a useful way to delete associations. For instance, to remove all associated tags from a user:

User.first.update(tag_ids: [])

Let’s try a PATCH request with an tag_ids array:

# Server log:#params
=> <ActionController::Parameters {"user"=>{"tag_ids"=>["null"]}, "controller"=>"users", "action"=>"create"} permitted: false>
#user_params
<ActionController::Parameters {"tag_ids"=>["null"]} permitted: true>

That’s not quite what we wanted, but it made it through the strong params filter. How would an ActiveRecord behave with [“null”]?

User.first.update(tags: ["null"])User Load (0.3ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1  [["LIMIT", 1]](0.1ms)  BEGINTag Load (0.2ms)  SELECT "tags".* FROM "tags" WHERE "tags"."id" = $1  [["id", 0]](0.1ms)  ROLLBACKTraceback (most recent call last):1: from (irb):8ActiveRecord::RecordNotFound (Couldn't find Tag with 'id'=[0])

Rails fired a database query looking for a Tag with an id of 0 — interesting, but not at all helpful. The question remains: how can we get an empty array into our params?

Under the hood, forms are strings — the key/value pairs don’t have a concept of null. The solution is to assign a value of and let Rails do the rest:

# Server Log#params
=> <ActionController::Parameters {"user"=>{"tags"=>[""]}, "controller"=>"users", "action"=>"create"} permitted: false>
#user_params
<ActionController::Parameters {"tags"=>[""]} permitted: true>

How does ActiveRecord behave with [“”]? Exactly the same as [].

User.first.update(tag_ids: [""])User Load (0.3ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1  [["LIMIT", 1]](0.1ms)  BEGINTag Load (0.2ms)  SELECT "tags".* FROM "tags" WHERE 1=0Tag Load (0.3ms)  SELECT "tags".* FROM "tags" INNER JOIN "user_tags" ON "tags"."id" = "user_tags"."tag_id" WHERE "user_tags"."user_id" = $1  [["user_id", 1]]UserTag Destroy (0.3ms)  DELETE FROM "user_tags" WHERE "user_tags"."user_id" = $1 AND "user_tags"."tag_id" IN ($2, $3, $4, $5, $6, $7, $8, $9)  [["user_id", 1], ["tag_id", 1], ["tag_id", 2], ["tag_id", 8], ["tag_id", 11], ["tag_id", 12], ["tag_id", 14], ["tag_id", 27], ["tag_id", 31]](1.1ms)  COMMIT=> true

Success!

Full stack web developer with a passion for number theory and algorithms.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store