ruby-on-railsrubydiscourseactionpack

Rails: What does calling ActionController::Parameters#permit() achieve when not using the object returned by it?


I understand that calling params.permit(:foo) creates a new ActionController::Parameters instance with :foo whitelisted so that you can instantiate a model with it. But why does the following code in the Discourse CMS call it without using its return value?

discourse/app/controllers/drafts_controller.rb: Github

class DraftsController < ApplicationController
  # [...]

  def index
    # [...]
    params.permit(:offset)
    params.permit(:limit)

    # [...]

    opts = {
        # [...]
        offset: params[:offset],
        limit: params[:limit]
    }

    stream = Draft.stream(opts)

Solution

  • This does look confusing, I agree.

    Judging from the implementation of #permit and the documentation of ActionController::Params, this can behave differently depending on the config action_on_unpermitted_parameters, which accepts :log and :raise as values and is nil by default.

    When action_on_unpermitted_parameters = nil:

    Calling params.permit(:foo) will return a new ActionController::Parameters instance marked as permitted with just that key.

    If you're not using the return value, this call makes little sense as there's no side effect. The receiver is not mutated.

    When action_on_unpermitted_parameters = :log:

    This behaves the same as above, but has the side effect of logging all not permitted keys:

    irb> ActionController::Parameters.action_on_unpermitted_parameters = :log
    => :log
    irb> params = ActionController::Parameters.new(username: 'john', offset: 5, bogus: 'foo')
    => <ActionController::Parameters {"username"=>"john", "offset"=>5, "bogus"=>"foo"} permitted: false>
    irb> params.require(:username)
    => "john"
    irb> params.permit(:offset)
    Unpermitted parameters: :username, :bogus
    => <ActionController::Parameters {"offset"=>5} permitted: true>
    irb> params.permit(:limit)
    Unpermitted parameters: :username, :offset, :bogus
    => <ActionController::Parameters {} permitted: true>
    

    As you can see, for each permit call, you'd get different logs. Thus, this would only make sense if the code in that controller would include all permitted (and required) parameters:

    irb> params.permit(:username, :offset, :limit)
    Unpermitted parameter: :bogus
    => <ActionController::Parameters {"username"=>"john", "offset"=>5} permitted: true>
    

    When action_on_unpermitted_parameters = :raise:

    The effect here is that it raises when the params contain keys that are not allowed. Similar to :log, this also only would make sense when all permitted (and required) keys are specified:

    irb> ActionController::Parameters.action_on_unpermitted_parameters = :raise
    => :raise
    irb> params = ActionController::Parameters.new(username: 'john', offset: 5, bogus: 'foo')
    => <ActionController::Parameters {"username"=>"john", "offset"=>5, "bogus"=>"foo"} permitted: false>
    irb> params.require(:username)
    => "john"
    irb> params.permit(:offset)
    Traceback (most recent call last):
            1: from (irb):19
    ActionController::UnpermittedParameters (found unpermitted parameters: :username, :bogus)
    irb> params.permit(:limit)
    Traceback (most recent call last):
            2: from (irb):20
            1: from (irb):20:in `rescue in irb_binding'
    ActionController::UnpermittedParameters (found unpermitted parameters: :username, :offset, :bogus)
    

    Contrast that to including all keys:

    irb> params.permit(:username, :offset, :limit)
    Traceback (most recent call last):
            2: from (irb):21
            1: from (irb):21:in `rescue in irb_binding'
    ActionController::UnpermittedParameters (found unpermitted parameter: :bogus)
    

    Having said that, I couldn't find any occurrence of action_on_unpermitted_parameters in Discourse's codebase. Thus, the value is nil and therefore I conclude that #permit in that controller action has no effect in terms of functionality.

    It could be there as a convention serving as documentation where first all required parameters are listed and then all optional ones.

    Digging deeper, these #permit calls were introduced in this commit when it was still a separate gem called strong_parameters. The behavior of #permit in that gem was the same as today. This makes me think that the author of that commit misunderstood the API of strong_parameters.