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. Themake_response!
method generates anActionDispatch::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 oteach
orcall
. A Body that responds toeach
is considered to be an Enumerable Body. A Body that responds tocall
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.