Rails Strong Params for GET Request

Published 03 Oct 2021 · 5 min read
Learn how to use Rails strong parameters with an HTTP GET request.

If you've been using Rails for a while, you've probably encountered Strong Parameters. This feature was introduced in Rails 4 and is intended to prevent mass assignment. The typical use case for this is to protect a POST or PUT/PATCH endpoint, which is invoked when a user submits a form and a controller action tries to create or update the corresponding model.

This post will demonstrate how strong params can also be used to protect a GET request. Why would you ever want to do that? I'll get to that in a minute. First, let's look at the typical case for strong params.

Typical Case: POST

Given an HTML form such as:

<form action="/person">
  <input type="text" name="person[name]" id="person_name">
  <input type="text" name="person[email]" id="person_email">
  <button type="submit">Create</button>
</form>

When the user fills out this form and clicks submit button, it will invoke a POST http action to the /person endpoint, with the following parameters:

{
  "person"=>
    {
      "name"=>"fred",
      "email"=>"fred@example.com"
    }
}

The corresponding PersonController#create method is responsible for creating a new Person model with the name and email properties populated and saving to the database. A naive implementation would look like:

class PeopleController < ActionController::Base
  def create
    Person.create(params)
  end
end

But what if a sneaky user modifies the form or uses a REST client to submit an additional parameter that happens to be used internally, but is not intended to be set by a user such as is_admin, for example:

{
  "person"=>
    {
      "name"=>"fred",
      "email"=>"fred@example.com"
      "is_admin"=>"true"
    }
}

The Rails solution is to use strong parameters to only allow the name and email properties of the form to be saved, rather than passing the entire user generated params to the model create method:

class PeopleController < ActionController::Base
  def create
    Person.create(person_params)
  end

  private

  def person_params
    params.require(:person).permit(:name, :email)
  end
end

Now, any additional parameters submitted to the /person endpoint will be discarded when Person.create is called because the person_params method only allows name and email.

Unusual Case: GET

Consider another case where strong params could be useful. Suppose your application exposes a GET /search?q=something endpoint, but behind the scenes, the actual search action is delegated to a third party service.

The search form accepts a search term from the user named q. Since this is a query and doesn't modify any state, the GET method is used.

<form action="/search" method="get">
  <input type="text" name="q">
  <button>Search</button>
</form>

The search action is handled by the search controller, which passes on the params given to it to the third party search service to perform the actual search:

class SearchController < ActionController::Base
  def search
    ThirdPartySearchService.call(params)
  end
end

Where the incoming params look like this:

{
  "q"=>"something the user typed into the form"
}

Since params comes from the user, it would be useful to restrict the fields that get sent to the third party search service to only the expected fields. In this simple example there's only one expected field q.

This sounds like a job for strong parameters! But recall the typical example is for protecting a model:

class PeopleController < ActionController::Base
  def create
    Person.create(person_params)
  end

  private

  def person_params
    params.require(:person).permit(:name, :email)
  end
end

This doesn't seem to be a good fit for a controller handling a GET request because the search controller is not creating or updating a model. Specifically, this line seems like a magic incantation:

params.require(:person).permit(:name, :email)

Here we get to the point where as a newcomer to Rails, it's very easy to copy/paste these seemingly magical incantations and as long as your usage is the same as the typical case, it will work just fine. However, sometimes your use case may be a little different, and the copy/paste no longer works. In this case, it requires digging in a little to understand what's going on "under the hood" so to speak.

TL;DR

For those that just want the solution real quick, here it is - simply call permit on the params with the single required field:

class SearchController < ActionController::Base
  def search
    ThirdPartySearchService.call(search_params)
  end

  def search_params
    params.permit(:q)
  end
end

To understand why this works, read on...

Deeper Explanation

To make strong parameters work for the search controller, we need to understand what params is. It appears to be a hash of inputs to the controller, that come from either an HTML form, url, or query parameters.

However, it's actually a method that returns an instance of ActionController::Parameters. This instance is initialized with a hash of the input parameters. It also has methods like require and permit which return new instances of ActionController::Parameters so that's why they can be chained together like params.require(:something).permit(:somefield, :otherfield).

require

Looking at the api docs for the require method, it accepts a key, and returns the value from the hash if the key exists, otherwise it raises ActionController::ParameterMissing. If the value happens to be a hash, then it returns a new instance of ActionController::Parameters initialized with the value. This means that given input parameters to a controller such as:

{
  "a"=>"some value",
  "b"=>"something else",
  "c"=>{
    "d"=>"in a hash"
  }
}

The following could be run in the controller:

params.require(:a)  # "some value"
params.require(:x)  # *** ActionController::ParameterMissing Exception: param is missing or the value is empty: x
params.require(:c)  # <ActionController::Parameters {"d"=>"in a hash"} permitted: false>

permit

Looking at the api docs for the permit method, it accepts a list of filters, and returns a new instance of ActionController::Parameters containing only fields that were specified in the filters. Carrying on with the a,b,c hash example:

# returns new instance of params with ONLY the `b` field
params.permit(:b)  # <ActionController::Parameters {"b"=>"something else"} permitted: true>

Notice that ActionController::Parameters is not tied to a model, it simply operates on a hash. This is why it can be used in any controller, even when not dealing with a model. Also, there's no rule that says you first have to call require, and then chain with permit. You're free to use any combination of these as it suits the use case.

Solution Explained

Now getting back to the search controller, in this case, there are no required fields (let's assume the service will do something reasonable like returning a default set of results when not given a query). The only requirement is that the controller should only pass the q field on to the service and nothing else. This is why it calls the permit method on params, specifying the q field, and then passes the resulting new params object to the third party service:

class SearchController < ActionController::Base
  def search
    ThirdPartySearchService.call(search_params)
  end

  def search_params
    params.permit(:q)
  end
end

Conclusion

This post has covered the typical use case for strong parameters in Rails and how it can also be used for a non typical case of an http GET request. It also took a deeper dive into some seemingly Rails "magic", to explain some methods of controller parameters.