Rails Controller Hierarchy

Metal Controller in Rails

The ActionController::Base class is the great-grandfather of all Rails controllers, but doesn't get much attention. In this post, we will examine this Metal class and how it works. We'll also trace the path of an incoming HTTP request to Rails controllers.

3 min read

All Rails controllers inherit from the ApplicationController class, which inherits from ActionController::Base class. This class itself inherits from the ActionController::Metal class.

The ActionController::Metal class is a stripped-down version of the ActionController::Base class. You can use it to build a simple controller as follows:

class HelloController < ActionController::Metal
  def index
    self.response_body = "Hello World!"
  end
end

To use it, we need to tell the router.

Rails.application.routes.draw do
  get 'hello', to: HelloController.action(:index)
end

The action method

The action method returns a Rack application, an object that implements the Rack interface. Upon receiving a request to /hello, the Rails router dispatches it to this Rack app, which returns the response.

Here’s the body of the action method.

# Returns a Rack endpoint for the given action name.
def self.action(name)
  app = lambda { |env|
    req = ActionDispatch::Request.new(env)
    res = make_response! req
    new.dispatch(name, req, res)
  }
  
  if middleware_stack.any?
    middleware_stack.build(name, app)
  else
    app
  end
end

The Rack application returned by the action method does the following.

  • Create a new HTTP Request using the Rack environment.
  • Call the make_response! method, passing the request. The make_response! method generates an ActionDispatch::Response object and assigns the request to it.
def self.make_response!(request)
  ActionDispatch::Response.new.tap do |res|
    res.request = request
  end
end
  • Finally, create a new instance of the controller class and dispatch (send) the request and response objects to the action (index in our case).
new.dispatch(name, req, res)

Dispatching the Action

The dispatch method takes the name of the action, the request, and the response, and returns the Rack response, which is an array containing the status, headers, and the response body.

def dispatch(name, request, response)
  set_request!(request)
  set_response!(response)
  process(name)
  request.commit_flash
  to_a
end

The first two lines set the controller instance variables for request and response for later use. Then, the process method, which is defined in the AbstractController::Base class, calls the action going through the entire action dispatch stack.

# AbstractController#process
def process(action, *args)
  @_action_name = action.to_s
  
  unless action_name = _find_action_name(@_action_name)
    raise ActionNotFound.new("The action '#{action}' could not be found for #{self.class.name}", self, action)
  end
  
  @_response_body = nil
  
  process_action(action_name, *args)
end

The process_action method calls the method to be dispatched (it can be different from the action name).

def process_action(method_name, *args)
  send_action(method_name, *args)
end

The send_action is an alias to the Object#send method, which actually calls the method associated with the action.

Typically, you invoke the send method on an object, like obj.send(:hello) which will call the hello method on the obj instance. In this case, however, as we didn’t specify an object, it calls it on the self object, which is the instance of the HelloController. Hence, it calls the HelloController#index method.

Processing the Action

Now let’s inspect our index action method. All it does is set the response_body on self, which is an instance of HelloController.

def index
  self.response_body = "Hello World!"
end

Let’s walk upwards in the stack. If you remember, our action was called by the AbstractController::Base#process method, which was invoked by the ActionController::Metal#dispatch method. Let’s continue where we left off in the dispatch method.

def dispatch(name, request, response) # :nodoc:
  set_request!(request)
  set_response!(response)
  process(name)  # we are here!
  request.commit_flash
  to_a
end

After calling process(name), the response_body is now set. The Request#commit_flash method deals with the flash, which we’ll ignore for now. Finally, it calls the to_a method, which delegates to the to_a method on the response.

def to_a
  response.to_a
end

Sending the Response

The ActionDispatch::Response#to_a method turns the Response into a Rack response, i.e. an array containing the status, headers, and body.

def to_a
  commit!
  rack_response @status, @header.to_hash
end

The rack_response method generates the body using the RackBody class, which responds to the each method, as it’s part of the Rack specification.

The body must respond ot each or call. A Body that responds to each is considered to be an Enumerable Body. A Body that responds to call is considered to be a Streaming Body.
def rack_response(status, header)
  if NO_CONTENT_CODES.include?(status)
    [status, header, []]
  else
    [status, header, RackBody.new(self)]
  end
end

So, ultimately, that’s how our simplest metal controller sends the response to an incoming HTTP request.