Developers are lazy. While reviewing an API, I’ve seen a lot of cases, that the only response codes returned were 500 (aka “backend is not working”) and 400 (broadly known as “frontend messed up”). “Better” APIs were also returning 404 and authentication/authorization failure codes. Such an approach is fast to implement and covers most of the use cases. But have you ever wonder, what will happen if developers have the possibility and time to experiment? They will delve into it.
This article has to make you pay attention to the quality of messages returned by your API, with the biggest focus on status codes. Selecting them carefully will make your API more descriptive and easier to use, reducing feedback loop time needed for root cause analysis.
The need for proper status
One day in my work, we were implementing some API, that was consuming JSON. Nothing special, but there was some logic behind two fields sent in a message payload.
Generally, the time difference between dates could not be bigger than some period. Tests, that we wrote for this endpoint, were checking if HTTP 400 Bad Request was returned in case of the too broad time range. Looked solid, but during code review, we got a comment, if is this a good status code, as request syntax is perfectly valid, but semantics is here a problem. Well, that started a very long discussion.
Word on Client Error codes
In general, response codes classes start with very broad information. For Client Error codes it is HTTP 400 Bad request, originally meaning “request sent by client has a malformed syntax”, which was very strict about the reason behind this status. Later, in updating RFC 7231, the definition of 400 was defined less restrictive. Since 2014, it is just information about potential client problem, that sent an invalid request. Neither specific nor descriptive. Hopefully, there are a lot of other status codes, that could be used. Looking on a list published on IANA or Mozilla Developer Center, we can see that they are more and more specific, from authorization issues, through invalid header values, to legal problems, so probably there is an error code that will fit our needs. In our case, we got a suggestion that 422 Unprocessable Entity could be used.
Doubts about 422
Request that we sent was perfectly fine in the matter of syntax, as the content type was understood by the server, and its body was successfully parsed. The problem was just in data that was provided within the body, which made our case a perfect situation for Unprocessable Entity status. Such a situation might happen for some reasons, e.g. because of logic behind command processing, like a requirement that selected period cannot be bigger than 60 days. That could be achieved by changing
userRegisteredTo field value to out of the defined range.
Even if requested command syntax was correct, data inside it was not. It can also happen in other scenarios, that check the quality of provided data, like in one below, where a user wants to set prices for shipping packages to some country but is trying to use the currency that cannot be used within it:
Even if our cases looked like a perfect match for 422 status code, we still had concerns regarding if it should be used. It is not a part of the standard HTTP protocol but is a part of its extension called WebDAV. WebDAV is used for managing resources, usually files, directories, and its properties. As we were not implementing anything related to WebDAV, it looked like an overhead. We were going to use only one response code defined in whole, over 120 pages documentation. We liked the response code, but we needed proof that we can use it.
To get an argument for using code from extension to plain HTTP, we need to take a step back and check original documentation one more time. Fortunately, in a section that is describing status codes there is one, very important sentence for us:
HTTP status codes are extensible.
This opens a lot of doors for using available extensions. There is a huge benefit in using specific response codes, as it provides information about the root cause of the problem. Knowing what was wrong, API clients can perform retry, tell users to fix the data, or authorize again, depending on retrieved status. It could not be possible with returning general HTTP 400 Bad Request in response.
Take the benefit of various response code, and inform clients about the source of a problem. If no code will match your need, remember that HTTP statuses are extensible, so look for best one also within extensions, or as a last resort — create your own one. If that is not enough for your case, consider implementing Problem Details for your API. Problem details can give huge insight for a user that is using your API, e.g. when requested setting price for shipping in invalid currency, apart from status code, it could provide response body like:
"title": "Requested currency cannot be used in this country.",
"detail": "Germany supports only currencies: EUR, BTC",
Format of response is standardized, if you are interested in it, please refer to the documentation.
Descriptive responses from your API may accidentally expose valuable information for attackers. You should not expose information about your infrastructure (webserver implementation, database version), or used language and its version. Make sure to never return any stack trace within your response body. All this information can allow malicious users to use exploits and break your application. You can also perform some additional steps to protect yourself, as not always returning detail is desirable behaviour.
Hiding what should not be visible
One of the things, that can improve your API security, is hiding details from a user that do not have access to a resource. Let’s visualize it with resource providing business deal details under
/api/contracts/32167. Who should be able to see those details? Probably only allowed users. Even more, probably not allowed users should even not know if such resource exists. Therefore, if an unauthorized user
A would request for this contract details, it could get a response 404 Not Found (instead of 403 Forbidden), which will hide from him even existence of such agreement.
A similar situation should happen for a user that is not logged in. Instead of returning 403 Forbidden for protected resources and 404 Not Found for not existing ones, it might be valuable to return 401 Unauthorized for all non-public resources. Usually, users, who are not authorized, are not consumers of API, so they should not retrieve detailed reasons.
Design API responsibly
Meaningful responses are simplifying communication, leaving no room for guesses that may lead to long hours for analysing the root cause. Being strict and precise in responses will cause your API more discoverable and easier to use for your clients. Just make sure, that you will not tell them too much.