API calls and HTTP Status codes

Why you shouldn’t use status 200 for nearly everything

This is a conversation I’ve had many times, and it usually ends up in an argue because it’s highly opinionated.

The use case

Let’s say you have an API running on some web server, doesn’t matter which one. There’s a login endpoint and some other resources that can’t be accesible unless you have already logged in into the web application. To request any of these resources you enter some credentials, user and password, and the login is denied.

What status code should the server return?

200 The request is successful as the endpoint does exist and makes some internal validation, but the response has to include some information on why the access is denied.

as the endpoint does exist and makes some internal validation, but the response has to include some information on why the access is denied. 401 The access is not authorized. No extra information required on the response.

No extra information required on the response. 403 The access is forbidden and the response includes information on access denied.

and the response includes information on access denied. 503 Cannot do anything unless user login is validated and authorized ( service unavailable ).

). Other status code (let's get creative)

I’ve seen this kind of questions on technical tests for some job applications.

Usual answers vary between the 200 and the 401 status codes, and some variations in between. At first I answered that this depends on the API implementation, so both codes are valid, but a second thought lead my answer to a 401 status code with some extra information even though the code itself should be enough.

To explain this I’ll use the 404 code as a reference, and an example that fits the explanation.

The image server

Lets suppose you’re developing an application that has to access some images from an API. Every image lives on an /img folder but some images with a “protected-” prefix on their filename require user-password authorization.

Your application is meant to work regardless on how this restrictions are implemented. (boldface means important)

Directly using nginx configuration, it would be something like:

location ~* \.(protected-.*\.)(jpg|gif|png)$ {

auth_basic “Restricted”;

auth_basic_user_file /etc/nginx/.htpasswd;

}

Nginx configuration is something like that, not saying it's exactly like the code above or that the code will work.

Basically it applies a basic auth "security" layer over all images that match protected-*.* asking for a user/password pair if any of these images is requested.

Apache web server has something similar. More on this on:

So if the app requests the file https://image-server/img/house.jpg it should return a 200 status and the requested file house.jpg

If the app requests the file https://image-server/img/protected-secret.png it should return a 401 code and require user credentials. If the user credentials are given (as authorization headers) and fail to login, a 401 code is returned. If the login is successful, the request will return a 200 code and the requested protected-secret.png file (if the file exists, otherwise it should return a 404).

Let’s say the app requests the file yoda.gif and there’s no Yoda file on the server. You’ll get a 404 code, file/resource not found.

What about a request for a non existent protected-vader.jpg file? It will first ask for credentials (that’s a 401 code) and if the credentials are successfully validated, a 404 code, because the resource is not found.

API calls are resource requests

For a server every endpoint is a resource, so every API call is also a resource request. So at the end it doesn’t matter how your resources are served, unless you feel like using the wrong codes for everything, like a 400 for successful requests (please don’t…).

To explain this I will show another implementation that does the same. For this I’ll use NodeJS and Express, you can do the same with any other language.

// […]

// Libraries, requires and else above this lines

// WARNING: This is not a complete copy & pastable code const validatePwd = (req, res, next) => {

// do validation pseudo-code ahead

// check if file has to be protected

if((req.param[‘imgfile’]).indexOf(‘protected-’) === 0){

// validate user and password

if( user:password are not authorized) {

res.status(401)

.json({

statusCode: 401,

error: true,

msg: ‘Invalid credentials’

});

} }

// else continue

next(); }; app.get(‘/img/:imgfile’, validatePwd, (req, res) => {

// if the endpoint flow has continued here it means the file

// is not protected, or that user:password are valid // Build a real file path to get the image file. storagepath

// is the real folder where images are stored

let imgpath = path.join(storagepath, req.params[‘imgfile’]); // Return the image file only if it exists, otherwise return a 404

if(fs.existsSync(imgpath)){

res.writeHead(200, {‘content-type’:’image’});

fs.createReadStream(img).pipe(res);

} else {

// File does not exist

res.status(404)

.json({

statusCode: 404,

error: true,

msg: `File ${req.params[‘imgfile’]} not found` }); }

});

But wait, even if the credentials are correctly validated and the access is authorized, if a request is done to a non-existent file such as protected-vader.jpg the request is served through an existent API endpoint. Shouldn’t that return a 200 code? The API endpoint does exists and it already did every required action to validate credentials and file existence.

Right. That’s what I meant the whole time. The endpoint (in this context I prefer calling them resources) does exist.

If you restrict your understanding of HTTP status codes to:

200 the resource exists (and handles the request)

404 the resource does not exist

there’s at least something you’re not getting about backend web development.

The /img/:imgfile endpoint will always exist, and it will always send a response. But there’s no sense for this response to be a 200 code with extra information if the file does not exist, it’s like having the same answer for success and failures:

200 :thumbsup Good, your request succeeded

200 :thumbsup Good, your request failed

same applies to server authorizations, and can’t think of any HTTP status code that escape this logic. There’s no sense on responding 200 for every request your API is handling.

If this happens every client app should handle errors internally as if they were successful requests, and the code becomes immediately so tightly coupled that changing the API (considering the basic requirement on The image server section) will require rewritting the app’s code for API calls.

Tight coupling is never a good sign for whatever code you’re working on.

Most of the times, if I have to design an API, I think on how it should be solved using plain server configurations; and if any extra information is required as a response, I usually send some internal status code and message, with any extra data the request is answering.

The only scenario where a code 200 could be a valid response code for every request is when you don’t mind about tight coupling, nor rewriting client app code, or you’re the only developer in charge of developing APIs, apps and else (and you don’t mind eating your own sh*t). Even then I still disagree on writing crappy, tightly coupled code; I will never ever ever enforce bad development practices.

Double think your API response codes; every developer beside you, who’s consuming the APIs you developed, will thank you for that (or will not hate you, hate you less, love you more, etc.)

Further references and explanations on https://www.restapitutorial.com/httpstatuscodes.html