'render' is not your final word in your Rails controller's action

… and check why 5600+ Rails engineers read also this

Today I have a quick tip for you. Suppose you are using some library that performs some logic in your controller and render some templates once some invariants are not met. Assume the library is old and was created once you were not using JSON API but was only rendering HTML templates. Now you need to add similar logic to your API controller. You may think that you should now modify the library to handle JSON responses, but that’s not the only solution you can use.

Remember that the render method in your controllers is not returning the control flow from your action and you can still modify the response or perform some operations once you call render. You only cannot call render more than once in a single action as that will raise the DoubleRenderError exception.

This means you can enhance your action without touching the library. Let’s assume your library is exposing a module with a method that is rendering a template in case on an exception (I’m not going to discuss if the library is well-written here, this in only an example):

module ActivationCheck

  def check_active(id)
    ActivationChecker.new.call(id) # returns true if a project with given id is active, raises an error otherwise
  rescue ActivationCheck::Error => exc
    render("activation_check/error", message: exc.message)
    false
  end
end

and you API controller looks like this:

class ProjectsController
  include ActivationCheck

  def show
    project = Project.find(params[:id])
    if check_active(project.id)
      render json: project
    end
  end
end      

The problem with using the library in your controller is that when the check_active method returns false, it also means the HTML template with 200 OK status will also be rendered in the response. You can always create some JSON template to overwrite the default HTML template provided by your library, but this will still return 200 OK status (and you should not return successful status code if your response is not successful). In order to handle this, let’s just modify your response status directly later in the flow:

class ProjectsController
  include ActivationCheck

  def show
    project = Project.find(params[:id])
    if check_active(project.id)
      render json: project
    else # in this branch ActivationCheck has already rendered a template
      response.status = :bad_request
    end
  end
end      

Now, as long as you create a JSON template to overwrite the default one (e.g. app/views/activation_check/error.json.erb), you will return the JSON response with proper status code in your controller without modifying the original library.

You might also like