DEV Community

Derek Hopper
Derek Hopper

Posted on

How to choose the right HTTP status code

Take a look at the following code. It's a jQuery $.ajax request asking for a cast member of The Office.

const message = document.querySelector('#message');

$.ajax({
  url: "/status/Michael",
  dataType: "json",
  success: (data) => {
    if (data.error) {
      alert(data.error);
    } else {
      message.innerHTML = data.message;
    }
  }
});

If the response includes an error property, the request is considered an error and we alert the user. Otherwise, a message is shown about the character.

This works. However, it's a little contradictory and some might consider it a code smell. After all, we're handling errors inside the success callback. This is a clue that something should likely be changed on the server.

Since we have to handle both cases within the success callback, we have a fairly good idea the server is returning a 20x status code for both successful and bad requests. Taking a peek at the server, this is what we might see:

class StatusesController < ApplicationController
  CHARACTERS = ["Michael", "Dwight", "Angela"]

  def show
    if CHARACTERS.include?(params[:name])
      render json: { message: "#{params[:name]} is a character on The Office." }
    else
      render json: { error: "Oops. There's an error." }
    end
  end
end

This is the JavaScript we would like to write. We'd like to use both a success and an error callback. This code reads a little better now. We can clearly see what happens when a request is successful or a request has an error. We don't have to concern ourselves with a conditional.

const message = document.querySelector('#message');

$.ajax({
  url: "/status/Michael",
  dataType: "json",
  success: (data) => {
    message.innerHTML = data.message;
  },
  error: (jqXHR) => {
    alert(jqXHR.responseJSON.error);
  }
});

Once the server returns a 404 Not Found, we can start using the error callback. We might update the server to look like this. Notice how we've added a status to both cases.

class StatusesController < ApplicationController
  CHARACTERS = ["Michael"]

  def show
    if CHARACTERS.include?(params[:name])
      render json: { message: "#{params[:name]} is a character on The Office." }, status: :ok
    else
      render json: { error: "Oops. There's an error." }, status: :not_found
    end
  end
end

Previously, as we know, the server was returning a 200 OK status code for both cases. After our update, :ok refers to the status code of 200 OK while :not_found refers to 404 Not Found. This allows our $.ajax call to route the response to the corresponding callback based on the response's status code.

This short example illustrates the importance of status codes and how a status code can affect anyone developing against it. When you're thinking about the implementation for a new endpoint or route, take a moment to think about what status code makes sense. A status code can have an effect on many things.

What to watch out for

Other developers

As you saw in our example, a status code can have an influence on how a frontend developer builds against the backend. Oftentimes, we need to fetch data from the backend and the status codes can influence how this is done.

If a status code is unexpected, you might see a weird bug. If we didn't know our server was returning a 200 OK for both successful and bad requests, our customers might run into interesting scenarios.

Security

Picking the wrong status code can provide clues to an attacker. For example, returning a status code other than 404 may give an attacker a clue that a given resource exists on the server.

Take a look at this excerpt from the GitHub API documentation on authentication.

There are two ways to authenticate through GitHub API v3. Requests that require authentication will return 404 Not Found, instead of 403 Forbidden, in some places. This is to prevent the accidental leakage of private repositories to unauthorized users.

If we give out clues that a private resource exists, we begin to lose trust with our customers.

SEO

If you have public endpoints, be careful when changing the status code. Using the wrong one can yield unexpected results.

When introducing a redirect, it's considered best practice to use 301 Moved Permanently. A search engine will pass along any link juice to the new URL. If you use a 302 Found, it might not.

Take a peek at respected literature on SEO and status codes if you're in doubt or you'd just like to learn.

Further Reading

If you're curious about status codes and what to do in certain situations, referencing popular frameworks is a good start. Take a look at what they're doing and consider how it may apply to your situation. Learning from others is key to growing your knowledge.

The responders gem (which was extracted and removed from Rails 5.0) has a suite of tests illustrating how you could use status codes in your application.

In this test, you can see 201 Created is expected when the request results in a resource being created.

def test_using_resource_for_post_with_xml_yields_created_on_success
  with_test_route_set do
    @request.accept = "application/xml"
    post :using_resource
    assert_equal "application/xml", @response.media_type
    assert_equal 201, @response.status
    assert_equal "<name>david</name>", @response.body
    assert_equal "http://www.example.com/customers/13", @response.location
  end
end

To handle an error, the test expects 422 Unprocessable Entity. It might be a good idea to use this status code when the user encounters a validation error.

def test_using_resource_for_post_with_xml_yields_unprocessable_entity_on_failure
  with_test_route_set do
    @request.accept = "application/xml"
    errors = { name: :invalid }
    Customer.any_instance.stubs(:errors).returns(errors)
    post :using_resource
    assert_equal "application/xml", @response.media_type
    assert_equal 422, @response.status
    assert_equal errors.to_xml, @response.body
    assert_nil @response.location
  end
end

Reference Material

What's your experience with status codes? Have you been a part of any interesting debugging sessions with status codes?

Discussion (0)