Just learn Rails (Part 3) HTTP status codes

So 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.