84

Several servers I have dealt with will return HTTP 200 for requests that the client ought to consider a failure, with something like 'success : false' in the body.

This does not seem like a proper implementation of HTTP codes to me, particularly in cases of failed authentication. I have read HTTP error codes pretty succinctly summed up as, '4xx' indicates that the request should not be made again until changed, while '5xx' indicates that the request may or may not be valid and can be retried, but was unsuccessful. In this case 200: login failed, or 200: couldn't find that file, or 200: missing parameter x, definitely seem wrong.

On the other hand, I could see the argument being made that '4xx' should only indicate a structural issue with the request. So that is proper to return 200: bad user/password rather than 401 unauthorized because the client is permitted to make the request, but it happens to be incorrect. This argument could be summarized as, if the server was able to process the request and make a determination at all, the response code ought to be 200, and it's up to the client to check the body for further information.

Basically, this seems to be a matter of preference. But that is unsatisfying, so if anyone has a reason why either one of these paradigms is more correct, I would like to know.

Yam Marcovic
  • 9,390

4 Answers4

68

Interesting question.

Basically, we can reduce this down to the right way to classify things in terms analogous to OSI layers. HTTP is commonly defined as an Application Level protocol, and HTTP is indeed a generic Client/Server protocol.

However, in practice, the server is almost always a relaying device, and the client is a web browser, responsible for interpreting and rendering content: The server just passes things on to an arbitrary application, and that applications sends back arbitrary scripts which the browser is responsible for executing. The HTTP interaction itself--the request/response forms, status codes, and so on--is mostly an affair of how to request, serve, and render arbitrary content as efficiently as possible, without getting in the way. Many of the status codes and headers are indeed designed for these purposes.

The problem with trying to piggyback the HTTP protocol for handling application-specific flows, is that you're left with one of two options: 1) You must make your request/response logic a subset of the HTTP rules; or 2) You must reuse certain rules, and then the separation of concerns tends to get fuzzy. This can look nice and clean at first, but I think it's one of those design decisions you end up regretting as your project evolves.

Therefore, I would say it is better to be explicit about the separation of protocols. Let the HTTP server and the web browser do their own thing, and let the app do its own thing. The app needs to be able to make requests, and it needs the responses--and its logic as to how to request, how to interpret the responses, can be more (or less) complex than the HTTP perspective.

The other benefit of this approach, which is worth mentioning, is that applications should, generally speaking, not be dependent upon an underlying transport protocol (from a logical point of view). HTTP itself has changed in the past, and now we have HTTP 2 kicking in, following SPDY. If you view your app as no more than an HTTP functionality plugin, you might get stuck there when new infrastructures take over.

Yam Marcovic
  • 9,390
30

This question is a bit opinion based, but either way.

The way i see it, 200 can serve "soft errors". When it comes to building API's i try to distinguish between these and "hard errors".

"Soft errors" will be served with a status code of 200, but will contain an error description and a success status of false. "Soft errors" will only occur when the result is "as expected", but not a success in the strictest sense.

It's important to note that "soft errors" are more of a hint to the implementer. Therefor it is important to also provide more information about the error such as a human-readable error message and/or some sort of code that can be used to provide the end-user with feedback. These errors provide the implementer (and end-user) with more information about what happened on the server side of things.

For instance say you have an API with a search function but during a search, no results are yielded. This is not erroneous, but it's not a "success" either, not in the strictest sense of the definition.

Example formatted as JSON:

{
    "meta" {
        "success": false,
        "message": "Search yielded no results",
        "code": "NORESULTS"
    }
    "data": []
}

"Hard errors" on the other hand, will be served with a status code which is recommended for the error. User not logged in? – 403 / 401. Malformed input? – 400. Server error? – 50X. And so on.

Again, it's a bit opinion-based. Some people want to treat all errors equally, "hard error" everything. No search results? That's a 404! On the other side of the coin, no search results? – This is as expected, no error.

Another important factor to take into consideration is your architecture, for instance; if you interact with your API using JavaScript XHR requests and jQuery or AngularJS. These "hard errors" will have to be handled with a separate callback, whereas the "soft errors" can be handled with the "success"-callback. Not breaking anything, the result is still "as expected". The client-side code may then look at the success-status and code (or message). And print that to the end-user.

mausworks
  • 768
  • 5
  • 10
19

There are two aspects of an API: The effort to implement the API, and the effort of all the clients to use the API correctly.

As the author of the client, I know that when I send a request to a web server, I may either get an error (never talked properly to the server), or a reply with a status code. I have to handle the errors. I have to handle a good response. I have to handle expected, documented, "bad" responses. I have to handle whatever else comes back.

Designing the API, you should look at what is the easiest for the client to process. If the client sends a well-formed request, and you can do what the request asks you to do, then you should give an answer in the 200 range (there are some cases where a number other than 200 in that range is appropriate).

If the client asks "give me all records like ...", and there are zero, then a 200 with success and an array of zero records is fully appropriate. The cases that you mention:

"Login failed" usually should be a 401. "Couldn't find file" should be a 404. "Missing parameter x" should be something around 500 (actually, a 400 if the server figures out that the request is bad, and 500 if the server is totally confused by my request and has no idea what's going on). Returning 200 in these cases is pointless. It just means as the author of a client, I cannot just look at the status code, I have to study the reply as well. I can't just say "status 200, great, here's the data".

Especially the "parameter missing" - that's not something that I would ever handle. It means my request is incorrect. If my request is incorrect, I don't have a fallback to fix that incorrect request - I would send a correct request to start with. Now I'm forced to handle it. I get a 200 and have to check whether there's a reply "parameter missing". That's awful.

In the end, there are a dozen or two status codes for handling many different situations, and you should use them.

gnasher729
  • 49,096
1

My approach is to return errors from the server based on client input as 200 responses with a custom response model. In Typescript it's typically like:

//TypeScript bug does not allow boolean as a union discriminator so we just use 0 or 1.
interface ISuccessBit {
    success: number 
}

//Error can be General or Form, more can be added when required. interface ErrorResponse extends ISuccessBit { success: 0 error: GeneralError | FormError, }

//Each error will have a type and a general message. interface AppError {
type: string, message: string }

interface GeneralError extends AppError { type: "GENERAL", }

interface FormError extends AppError { type: "FORM" invalidFields: FormFieldResponse[] }

interface FormFieldResponse { name: string value: any message: string }

interface SuccessResponse<T> extends ISuccessBit { success: 1, payload: T }

//We pass in the object that we are expecting the response to be. interface IResponseWrapper<T> { response: ErrorResponse | SuccessResponse<T> }

//Example expected DTO interface User { name: string, age: number }

//Example responses let successResponse: IResponseWrapper<User> = { response: { success: 1, payload: { name: "James", age: 5555 } } }

let errorResponse: IResponseWrapper<User> = { response: { success: 0, error: { type: "GENERAL", message: "Invalid Details." } } }

let formError: IResponseWrapper<User> = { response: { success: 0, error: { type: "FORM", message: "Form contained invalid fields.", invalidFields: [ { name: "Name", value: -1111, message: "Name must not contain numbers." } ] } } }

It's worked well for me in keeping everything organised and consistent, and sometimes I add an ErrorCode enum/ID to the ErrorResponse if the app could do with it. Then handle unexpected server HTTP 4xx/5xx responses using the message property.

By adding discriminated unions to the error types we make it scalable to fit specific components other than just general and form errors. And using a standard message field across all errors (the same way standard JavaScript Error interfaces do) we can have a centralised way of presenting to the user.