Just learn Rails (Part 3) HTTP status codes
20 Sep 2015So you want to be a Rails superstar? To live large, big servers, requesting tars. Writing code all over the world, gotta make commits constantly. Great, I feel the same way.
Assuming you have already had that “I should just learn Rails” moment, the idea of writing an API for a super awesome application might be on the horizon. And, of course, the framework that makes the most sense is Ruby on Rails. Given that is the direction things might naturally evolve, creating an API with Rails can gloss over a few crucial design concepts and considerations.
HTTP status codes are important
When the Hypertext Transfer Protocol (or HTTP) was conceived, the idea of a server responding with multiple distinct pieces of data came into existence. Officially, HTTP is a stateless application-level protocol for distributed, collaborative, hypertext information system. In other words, it is a standard way that one server can talk to another in a predefined fashion. This is especially useful when writing a system that will facilitate requests from someone other than the person which built it; e.g. an HTTP API.
The aspect of HTTP that is most relevant in this post are status codes. These codes explicitly tell consumers of an application how they should react to a request. From authorization issues to an incorrect arity of parameters, a large amount of information is exposed via HTTP status codes.
Rails status code support
Let us assume that an application exists to look up information about books. To get a specific book, a RESTful route is provided by the application at https://mycoolbookapp.com/books/12
. This route might execute the following controller action:
class BooksController < ApplicationController
def show
render json: Book.find(params[:id])
end
end
This simple controller action can respond with three HTTP status codes. A 200
will be in the response of any Book
which matches the :id
supplied. Likewise, a 404
will automatically be returned if the Book
requested does not exist. Finally, if some “code in need of improvement” is accessed and fails, a 500
is returned to let the user know that the server has almost certainly caught fire (surely no code pushed to production has bugs in it).
For a very simple API, this response structure is adequate. However, achieving more complicated responses might not be so straight forward. For instance, if a user must be logged in to access a Book
, the correct response might be 401
, to explicitly let the user know that they are not authorized to see that resource.
If HTTP status codes are unknown or underutilized, an inexperienced developer might write that response that attempts to only use the text body of a response.
class BooksController < ApplicationController
before_filter :authenticate_user
def show
if @user.nil?
response = { error: 'User is not logged in' }
else
response = Book.find(params[:id])
end
render json: response
end
end
This code will respond with a 200
and a message denoting an error. This is not the correct way to return a result. This is the equivalent of someone allowing you to leave a store with an item and not pay, then arresting you for it. It just does not make sense and should be avoided.
Instead, Ruby on Rails gives us very helpful symbols that can be used to accurately convey the appropriate response to end users.
To only return the correct HTTP status code, the books controller can be rewritten to:
class BooksController < ApplicationController
before_filter :authenticate_user
def show
if @user.nil?
return head :unauthorized
end
render json: Book.find(params[:id])
end
end
Additionally, you can provide even more information to the consumer of your API with a combination of HTTP status codes and a response body:
class BooksController < ApplicationController
before_filter :authenticate_user
def show
if @user.nil?
response = { error: 'User is not logged in' }
status = :unauthorized
else
response = Book.find(params[:id])
status = :ok
end
render json: response, status: status
end
end
Communicate effectively
This example has exposed the difference between a decent API and one that understands how HTTP should work. While the specific problem could be solved with CanCanCan, it is important to understand how and why those libraries work the way that they do.
If one were to continue down the naive path, returning hashes or strings with incorrect HTTP status codes, things would become messy quickly. That response structure unjustly handcuffs the API clients to be unnecessarily tolerant of ad-hoc text responses. But, if the API conforms to HTTP standards, a client knows exactly what each response means. A 400
status code in the response will give context to what a supplied error means. The client can respond correctly and know that their request resulted in an error. However, if a response body is the only way to denote that something went wrong, what stops a new developer on the API from changing the error
key to Error
and ruin everyone’s day?
Instead of capturing in text every single detail about why a request did not result in an expected response, use HTTP status codes to supplement and communicate effectively.