How to choose the right HTTP status code
Tags: webdev
Published: 2020-06-24 00:07:41

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

```javascript
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:

```ruby
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.

```javascript
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.

```ruby
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](https://developer.github.com/v3/#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](https://moz.com/learn/seo/http-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](https://github.com/heartcombo/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.

```ruby
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.

```ruby
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

- [Wikipedia: List of HTTP status codes](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes): A raw list of standard status codes.
- [Mozilla: HTTP response status codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status): Explains the common use cases for each status code.

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