Search code examples
ruby-on-railsrubyjsonruby-on-rails-4rendering

How to properly use `return` for `render`: DoubleRenderError


I'm new to json and experience a problem with returning. I get the error below, even though everywhere I render, I also return. What causes this error?

AbstractController::DoubleRenderError in Api::V1::ArticlesController#show
Render and/or redirect were called multiple times in this action.

The error refers to render json: {errors: "not authorized" }, status: :unauthorized in def authenticate_user below. Instead, I would expect it to just render a json error: "not authorized". Any idea why this is not happening?

before_action :authenticate
def authenticate
  unless authenticated?
    render json: {errors: "not authorized" }, status: :unauthorized
    return
  end
end

This calls on the following helper methods:

def authenticated?
  !current_user.nil?
end

def current_user
  token = request.headers["Authorization"]
  email = request.headers["HTTP_USER_EMAIL"]
  if token
    user = User.friendly.find_by(email: email)
    if user
      if user.token_expired?
        render json: {errors: "Session expired, please login" }, status: :unauthorized
        return
      elsif user.authenticated?(token)
        @current_user = user
      end
    end
  else
    nil
  end
end

Update: The solutions provided to remove return everywhere work, but I don't understand why. Suppose the helper method looks like the one below. Then it would be important to include return, right? because otherwise if @node isn't found, the method would still continue and the message "node not found" would not be displayed. Could you explain?

  def create
    @node = Node.find_by(id: params[:node_id])
    if @node.nil?
      render json: { errors: "node not found" }, status: :bad_request
      return
    end
    nodee = @node.create(create_params)
    if nodee.save
      render json: @node, status: :created
    else
      render json: @node, status: :bad_request
    end
  end

In other words, I would have expected that removing render from def authenticate would result in Rails just continuing to the create method, since def authenticate didn't tell it to step there (which is how I see render).

Update2: I can also not remove render as suggested in the answers, but instead move it to the beginning of the line: return render json: {errors: "not authorized" }, status: :unauthorized. Would love to understand why this is.


Solution

  • Your core issue is that current_user is doing a render itself - this is unusual and makes it very easy to accidentally render twice. The second thing is that a before filter halts processing of the action if render or redirect are called during the filter: returning from the filter doesn't directly impact things.

    Removing the return from current_user works largely by accident: this means that the return value from current_user is the return value from render, ie is no longer nil. This means that authenticated? returns true (even though the user isn't authenticated) so your before filter doesn't render a second time. Rails then stops execution of the action since render was called from within a filter so it appears that things have worked.

    In general though you do need to call return to stop the rest of the method executing, however this obviously doesn't stop process inside the calling method.

    Personally I would leave authenticate as is but change current_user so that it doesn't have any side effects (other than setting @current_user). If you really want to change the error message then track the reason why current_user is nil in a separate instance variable (in truth I probably wouldn't be rolling my own authentication but that's another story)