Photo by Erik Mclean on Unsplash

Navigating HTTP Status Codes for REST APIs

Joy Ebertz
Nerd For Tech
Published in
7 min readMar 15, 2021

--

I love status codes. I always felt like I’d finally be a real expert at APIs if I could make status code jokes. While my favorite status code is definitely 418, there’s a special place in my heart for 404. At one of my previous companies, 404 was my employee number and I always found it funny when it showed up on documents. I half expected my records to disappear and no one to notice. I’m sorry, Joy wasn’t found.

Figuring out the correct status code for a situation and remembering all of their meanings can be a bit tricky though. Similar to HTTP methods, HTTP status codes often present challenges when building your REST APIs. On the surface, the list of status codes seems very straight forward, it’s only once you start to dig into them that it becomes easy to see that there are a lot of cases where it isn’t clear which one to use. Multiple codes seem to apply sometimes and other times nothing seems to quite fit. So how do we navigate status codes? How do we choose which one applies to a particular scenario?

For all of these, I’m going to assume that there is some client that sends an API call to a server and the server is responding with a status code.

Deciding Between 4xx and 5xx

Typically, 4xx (any code in the 400s) is a “client error” and a 5xx (any code in the 500s) is a “server error”. The tricky bit is how exactly to interpret that. The general rule of thumb is: If it is the client’s fault that the request is failing (e.g. the parameters are invalid), return a 4xx, if it’s the server’s fault, then it’s a 5xx. One other way to think about this is if a client gets a 5xx, they should be able to expect to make the exact same call in the future and get a different response. Meanwhile, if the client gets a 4xx, they need to modify something in their request if they want to get anything other than the same 4xx back.

This starts to get tricky with a microservice architecture. If you get an error from a down-stream service, it can be tempting to just pass along that error. However, take the example where I have the client calling service A, which calls service B. If service B returns a 4xx because service A mangled the request to it, it may not in any way be the client’s fault, and therefore service A should return a 5xx, not a 4xx, to the client. However, if service B returned that same 4xx because the data that the client put into the original request was invalid, service A should return a 4xx. Ideally, for this second case, there would be enough data validation in service A such that we would return an error before we ever called service B. It’s easy to miss some data validation though, so it’s important to take a close look at service B’s errors when deciding what service A should return.

Deciding Between 400, 401, 405, 403 and 404

All of the 4xx responses can sometimes start to sound the same. In order to keep them straight, I go through the following decision process.

If the client sent a garbled request, (invalid JSON, invalid URI format, etc), then they should get back a 400.

If the request format was valid, but they didn’t provide valid authentication (expired or invalid API Key), then they should get back a 401. A 401 means that the client, with those credentials, cannot make a successful call to any endpoints (that require authentication).

If the authentication credentials were valid, but either the endpoint they’re trying to reach doesn’t exist (we don’t support /unicorns) or the specific ID they’re trying to access doesn’t exist (there’s no animal with an id of 456 when they call /animals/456), then they should get back a 404. Another way to think about a 404 is that it indicates an invalid URI path (i.e. everything before the ?query or #fragment). It is common for APIs to also return a 404 in some authorization error cases. The authorization case where a 404 applies is when the client is not allowed to know that the resource exists. Returning a different error code, such as a 403 would give the client information that the ID they just tried is valid, which, they shouldn’t know if they shouldn’t know of that ID’s existence. Therefore, a 404 is also commonly returned if, from the client’s perspective, the ID does not exist, regardless of what the server or anyone else may think or know.

If, the resource exists, and the caller has permission to view the resource in question, but that particular HTTP method has not been implemented, they should get back a 405. This is the case where the server allows clients to GET a resource, but not POST to that resource (for example).

If, however, some users have access to that particular method, just not the user with the current credentials, then we should return a 403. For example, if I’m trying to edit a resource that I have view-only access to, I should get back a 403. Other examples of 403s are if I’m trying to access an endpoint for a feature that my enterprise hasn’t purchased or if the API key that I’m using is not scoped for the endpoint I’m calling. The big difference between a 403 and a 401 is that with a 401, I cannot access any endpoint successfully. With a 403, I just can’t access this particular one.

If we’ve gotten past all of those, but some of the data in the request is still invalid. For example, I sent an invalid option for an ENUM field, or I passed in two different parameters that can’t both be accepted at the same time, then I should get back a 400.

If we’ve gotten past all of these, then another response code should likely be returned.

404 vs 204

404 and 204 can be a little confusing because it may seem at first that if an object doesn’t exist, then I might get back a 204 (valid call, but nothing there, thus the success with an empty body). However, this should not be a 204. A 204 is reserved for cases where the referenced resource exists but it doesn’t make sense to return the resource as a response. This can be either because the call is a successful DELETE call, so that the response should be empty since the resource no longer exists (although the resource existed before the call, so not a 404) or where something other than the standard CRUD was done — like in the case of a controller-resource, where we might want to indicate the successful execution of some function, but there isn’t really an appropriate response body or resource.

Empty Lists: 404 or 200?

On the surface this sounds very similar to the 404 vs 204 case, but it’s actually slightly different. If the client calls GET /users/{userId}, then the client is specifying that specific userId as a part of the URI. If that userId does not exist from the client’s perspective, the client should get a 404 (invalid URI). However. If the client calls GET /users?name=joy, then the client is asking for a list of all users with the name joy. In this case, the URI, /users does exist and is valid. If there is no one with the name joy, then we’ve just filtered that list down to nothing and the client should be getting a 200 back with an empty list. The URI is valid but the client just filtered out everything, so an empty list is returned.

400 vs 422 (And Other WebDAV codes)

The basic answer here is that if you’re not implementing a WebDAV API, you shouldn’t be returning any WebDAV error codes. There are cases where it may sound like a WebDAV error code may be more applicable than any of the others. However, it’s typically considered incorrect to return any of these if you aren’t WebDAV. The same applies to many of the unofficial or service specific codes. If you chose to try to implement any of these, just know that any standard response handlers that your clients are likely using won’t be able to handle the response or will handle it incorrectly.

This is a summary of the tricky cases I’ve come across recently, but I’m sure I’ve missed some. Let me know if you have any others that are hard to decipher. Also, as with anything that’s a bit grey around the edges, different teams and companies interpret some of these things slightly differently than I have. That highlights one thing that I think is even more important than getting any of this right — consistency within your own API and clear documentation for your APIs. It is important to clearly state what the error codes mean in your own API. Even better, providing custom error messages and documentation will do far more than getting any given error code exactly correct.

For further reading on REST APIs, check out my post on HTTP Methods.

--

--

Joy Ebertz
Nerd For Tech

Principal Software Engineer & ultra runner @SplitSoftware