How Rails handles status codes
Table of Contents
Recently, I have been building an API as part of my day job. Rails is a great framework to build APIs in, and it has been a joy so far. When building the responses of the API, it’s paramount to understand what HTTP statuses you should utilize, which will in return help the consumers by providing more meaningful responses.
Sure, you could always have a status
property in the response JSON, which
will be a human-readable status code. But, I would like you to think about HTTP
status codes as a nice card on a present, with your beautiful handwriting and
best wishes to whom the gift goes to. HTTP status code don’t just add more
semantical correctness, but they also speak about the nature of the response.
Rails and statuses #
Ruby on Rails, being a great framework, adds a very nice layer of abstraction of the status codes, and allows the developers to easily use custom status codes in their responses. I am sure, at some point you have seen something like:
render json: @resource, status: 201 # Created
While this might be sometimes redundant, the render
method in Rails allows
you to specify HTTP status codes for the response. But also, being very
developer friendly, Rails allows you to do the same with “human readable”
status codes:
render json: @resource, status: :created
The two code samples are identical, they specify the HTTP 201 status code to the response. But, what are the available Rails status codes (as symbols) that you can use? And how does Rails actually do this?
Status codes #
The HTTP status codes list is a quite stable and static list. Although sometimes status codes are added to the list, once you learn it, keeping up to date is rather easy. The latest addition to this list is HTTP 451 - Unavailable For Legal Reasons. You can see the RFC where it was proposed here.
So, how does Rails knows how to create all of these symbols, so we can use them
in our render
methods? Well, what’s very interesting is that Rails actually
doesn’t do much about this. It relies on Rack, which does all of this magic. In
an irb
session, type:
>> Rack::Utils::HTTP_STATUS_CODES
{100=>"Continue",
101=>"Switching Protocols",
102=>"Processing",
200=>"OK",
201=>"Created",
202=>"Accepted",
203=>"Non-Authoritative Information",
204=>"No Content",
205=>"Reset Content",
206=>"Partial Content",
207=>"Multi-Status",
208=>"Already Reported",
226=>"IM Used",
300=>"Multiple Choices",
301=>"Moved Permanently",
302=>"Found",
303=>"See Other",
304=>"Not Modified",
305=>"Use Proxy",
307=>"Temporary Redirect",
308=>"Permanent Redirect",
400=>"Bad Request",
401=>"Unauthorized",
402=>"Payment Required",
403=>"Forbidden",
404=>"Not Found",
405=>"Method Not Allowed",
406=>"Not Acceptable",
407=>"Proxy Authentication Required",
408=>"Request Timeout",
409=>"Conflict",
410=>"Gone",
411=>"Length Required",
412=>"Precondition Failed",
413=>"Payload Too Large",
414=>"URI Too Long",
415=>"Unsupported Media Type",
416=>"Range Not Satisfiable",
417=>"Expectation Failed",
422=>"Unprocessable Entity",
423=>"Locked",
424=>"Failed Dependency",
426=>"Upgrade Required",
428=>"Precondition Required",
429=>"Too Many Requests",
431=>"Request Header Fields Too Large",
500=>"Internal Server Error",
501=>"Not Implemented",
502=>"Bad Gateway",
503=>"Service Unavailable",
504=>"Gateway Timeout",
505=>"HTTP Version Not Supported",
506=>"Variant Also Negotiates",
507=>"Insufficient Storage",
508=>"Loop Detected",
510=>"Not Extended",
511=>"Network Authentication Required"}
As you can see, the Rack::Utils::HTTP_STATUS_CODES
is a Hash that has all of
the HTTP status codes, with the corresponding messages. If you open the
Rack::Utils
documentation
you can see how these are programmatically created, including the code that
does the conversion. The status codes are pulled from
this CSV file
and are parsed using this piece of code:
ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and puts "#{m[1]} => \x27#{m[2].strip}\x27,"'
Great. But, how do we get the symbolized versions of the status messages? Well,
Rack does that for us as well. In an irb
session, try this:
>> Rack::Utils::SYMBOL_TO_STATUS_CODE
{:continue=>100,
:switching_protocols=>101,
:processing=>102,
:ok=>200,
:created=>201,
:accepted=>202,
:non_authoritative_information=>203,
:no_content=>204,
:reset_content=>205,
:partial_content=>206,
:multi_status=>207,
:already_reported=>208,
:im_used=>226,
:multiple_choices=>300,
:moved_permanently=>301,
:found=>302,
:see_other=>303,
:not_modified=>304,
:use_proxy=>305,
:temporary_redirect=>307,
:permanent_redirect=>308,
:bad_request=>400,
:unauthorized=>401,
:payment_required=>402,
:forbidden=>403,
:not_found=>404,
:method_not_allowed=>405,
:not_acceptable=>406,
:proxy_authentication_required=>407,
:request_timeout=>408,
:conflict=>409,
:gone=>410,
:length_required=>411,
:precondition_failed=>412,
:payload_too_large=>413,
:uri_too_long=>414,
:unsupported_media_type=>415,
:range_not_satisfiable=>416,
:expectation_failed=>417,
:unprocessable_entity=>422,
:locked=>423,
:failed_dependency=>424,
:upgrade_required=>426,
:precondition_required=>428,
:too_many_requests=>429,
:request_header_fields_too_large=>431,
:internal_server_error=>500,
:not_implemented=>501,
:bad_gateway=>502,
:service_unavailable=>503,
:gateway_timeout=>504,
:http_version_not_supported=>505,
:variant_also_negotiates=>506,
:insufficient_storage=>507,
:loop_detected=>508,
:not_extended=>510,
:network_authentication_required=>511}
As you can see, the Rack::Utils::SYMBOL_TO_STATUS_CODE
constant contains all
of the status codes as symbols. The
documentation
also shows the actual conversion, from HTTP_STATUS_CODES
to
SYMBOL_TO_STATUS_CODE
:
Hash[*HTTP_STATUS_CODES.map { |code, message|
[message.downcase.gsub(/\s|-|'/, '_').to_sym, code]
}.flatten]
Having all of this documentation in place, is rather easy to see how everything falls into place. Rails being a Rack app, can utilize everything that Rack provides, and this is just an example. But, how does Rails actually plugs this into its runtime and has the ability to understand what is the status code?
A bit deeper #
Rails’ source code, although very well documented and well structured, can still be overwhelming to someone that hasn’t spent enough time digging through. If you are a Ruby developer, I encourage you to spend some time around Rails’ code - you will learn a lot, I promise you!
So, how does Rails know what status code to apply to the response, if you provide just the symbolized version on the status? Well, let’s start with Rails’ class hierarchy, which is quite nice.
On the surface, or what we usually see as developers, the rendering is done in
the controllers. If you open any of your applications’ ApplicationController
,
you will see something like:
class ApplicationController < ActionController::Base
...
end
This means that all of our controllers are subclasses of the
ActionController::Base
class. If you open the
source code
of the ActionController::Base
class, you will see some very interesting things,
that can be quite informative. But for our use case, we are interested in the
following:
MODULES = [
AbstractController::Rendering,
AbstractController::Translation,
AbstractController::AssetPaths,
Helpers,
UrlFor,
Redirecting,
ActionView::Layouts,
Rendering,
Renderers::All,
ConditionalGet,
EtagWithTemplateDigest,
Caching,
MimeResponds,
ImplicitRender,
StrongParameters,
Cookies,
Flash,
FormBuilder,
RequestForgeryProtection,
ForceSSL,
Streaming,
DataStreaming,
HttpAuthentication::Basic::ControllerMethods,
HttpAuthentication::Digest::ControllerMethods,
HttpAuthentication::Token::ControllerMethods,
# Before callbacks should also be executed the earliest as possible, so
# also include them at the bottom.
AbstractController::Callbacks,
# Append rescue at the bottom to wrap as much as possible.
Rescue,
# Add instrumentations hooks at the bottom, to ensure they instrument
# all the methods properly.
Instrumentation,
# Params wrapper should come before instrumentation so they are
# properly showed in logs
ParamsWrapper
]
MODULES.each do |mod|
include mod
end
This piece of code, actually includes all of these modules in the
ActionController::Base
class, which in return gives us all of the
functionlities that our controllers have. Just look at the module names, like
Redirecting
, ActionView::Layouts
, Rendering
, Cookies
, Flash
and so on.
Very self-explanatory, right?
Well, the classes that we are interested in are AbstractController::Rendering
and ActionController::Rendering
. The first one is an abstract class, kind of
like an interface, and the second one is the default implementation class, which
contains all of the rendering mechanisms that Rails uses to render your response.
When we call render
in our controllers, we are executing the
ActionController::Rendering#render
method, which does couple of things for us:
it normalizes the arguments, the options and then builds the body of the response.
When it normalizes the arguments, deep inside the _normalize_render
method, it
contains these lines of code:
if options[:status]
options[:status] = Rack::Utils.status_code(options[:status])
end
This piece of code, takes the :status
option from our render call, and
translates the symbol to an actual HTTP code. If we go back to Rack::Utils
documentation, we can see the actual implementation of the status_code
method:
# File 'lib/rack/utils.rb', line 573
def status_code(status)
if status.is_a?(Symbol)
SYMBOL_TO_STATUS_CODE[status] || 500
else
status.to_i
end
end
Here’s how your status: :created
gets translated to a status: 201
. As you
can see, although Rails’ source is sometimes hard to digest, it’s very well
done and navigating through it is not that hard.
What about exceptions? #
So now we know how Rails does the expected rendering, which was created by us, developers. But, what happens when Rails hits an exception of some sort, but it still has to recover from it and return a proper HTTP status code to the client?
Although at first you might expect some sort of a rescue
block in
ActionController::Metal
class, this is not the case. Open any Rails app you
have on your computer, and run in it’s root directory:
➜ rake middleware
use Rack::Sendfile
use ActionDispatch::Static
use ActionDispatch::LoadInterlock
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use ActionDispatch::RequestId
use Rails::Rack::Logger
*use ActionDispatch::ShowExceptions*
use ActionDispatch::DebugExceptions
use ActionDispatch::RemoteIp
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use ActionView::Digestor::PerRequestDigestCacheExpiry
run YourApp::Application.routes
Actually, the exceptions are handled in Rails’ middleware, more specifically in
the ActionDispatch::ShowExceptions
middleware class. It wraps a group of
exceptions and maps them to a specific status code. This is the code that does
the mapping:
module ActionDispatch
class ExceptionWrapper
cattr_accessor :rescue_responses
@@rescue_responses = Hash.new(:internal_server_error)
@@rescue_responses.merge!(
'ActionController::RoutingError' => :not_found,
'AbstractController::ActionNotFound' => :not_found,
'ActionController::MethodNotAllowed' => :method_not_allowed,
'ActionController::UnknownHttpMethod' => :method_not_allowed,
'ActionController::NotImplemented' => :not_implemented,
'ActionController::UnknownFormat' => :not_acceptable,
'ActionController::InvalidAuthenticityToken' => :unprocessable_entity,
'ActionController::InvalidCrossOriginRequest' => :unprocessable_entity,
'ActionDispatch::ParamsParser::ParseError' => :bad_request,
'ActionController::BadRequest' => :bad_request,
'ActionController::ParameterMissing' => :bad_request,
'Rack::Utils::ParameterTypeError' => :bad_request,
'Rack::Utils::InvalidParameterError' => :bad_request
)
...
end
end
All of the exceptions that can occur in the request/response lifecycle are mapped
to a specific status code, which will be returned by the middleware if an
exception is rescued. You can see this functionality in
ActionDispatch::ShowExceptions#call
method, which invokes the
render_exception
method if an Exception
is rescued:
def call(env)
request = ActionDispatch::Request.new env
@app.call(env)
rescue Exception => exception
if request.show_exceptions?
render_exception(request, exception)
else
raise exception
end
end
def render_exception(request, exception)
backtrace_cleaner = request.get_header 'action_dispatch.backtrace_cleaner'
wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
status = wrapper.status_code
request.set_header "action_dispatch.exception", wrapper.exception
request.set_header "action_dispatch.original_path", request.path_info
request.path_info = "/#{status}"
response = @exceptions_app.call(request.env)
response[1]['X-Cascade'] == 'pass' ? pass_response(status) : response
rescue Exception => failsafe_error
$stderr.puts "Error during failsafe response: #{failsafe_error}\n #{failsafe_error.backtrace * "\n "}"
FAILSAFE_RESPONSE
end
Now, when the exception is being rendered, the ExceptionWrapper
comes into the
picture. It will wrap the exception, and return an appropriate status code, which
is translated from the symbolized variant, to the number version, using:
def status_code
self.class.status_code_for_exception(@exception.class.name)
end
The status_code
method invokes the status_code_for_exception
method, which
converts the status code:
def self.status_code_for_exception(class_name)
Rack::Utils.status_code(@@rescue_responses[class_name])
end
And that’s about it. After that, the middleware stack is being executed in order to return the new response, with the exception and the proper HTTP status code attached.
After all #
Although all of this Rails “magic” might be a bit much to digest at first, Rails’ source code does quite a good job of explaining what is going on. And as you can see, there’s always something more than meets the eye. If you need to remember anything from this blogpost is that Rails does a very good (and interesting) job at handling HTTP status codes, to make your app’s responses semantically correct and client friendly.
If you got to this point - thanks so much for reading. I know it was a bit of a long journey, and I hope it was informative for you. Looking forward to reading your comments!