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.

10
  • 10
    success: false implies that the request failed and you know it. That should be a 500. Something like your bad username/password would be a 401. This isn't that ambiguous.
    – Pete
    Commented Dec 16, 2015 at 18:19
  • 4
    This is one of those questions that can invoke religious wars I think. For a RESTful API, the answer is clear, but there are other sorts of APIs where HTTP is treated just as a transport layer, and in those cases, application errors shouldn't bleed to that layer.
    – user53141
    Commented Dec 16, 2015 at 22:57
  • 10
    When I'm really not sure what http status to return it's always tempting to go with 418 "I'm a teapot."
    – joshp
    Commented Dec 17, 2015 at 3:59
  • 2
    WRONG. success: false does NOT imply the request itself failed. When dealing with web applications, the request is often NOT a simple "GET" request for a document identified by a URL. Rather the request is more often a POST to a remote method in an application (i.e. a remote procedure call). If anything, it's more like a "TRY GET", and you can't even use the HTTP GET verb because you can't post, for example, a JSON payload for the method to consume.
    – Triynko
    Commented May 25, 2017 at 19:54
  • 2
    success: false is NOT a 500. As a 500, an AJAX request through jQuery would not even be able to process the payload, as it considers anything other than a 200 (or 301 found) as a failure and invokes a failure handler. If the request makes it to the server, finds the right procedure, executes, and returns a valid, formatted response for the application... that's a 200. Always. Period.
    – Triynko
    Commented May 25, 2017 at 20:03

4 Answers 4

67

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.

4
  • 18
    Very insightful. The strongest argument here is the (impedance) mismatch between HTTP status codes and your app's return values. This can become a nightmare on the long run. Further, I highly support the separation of concerns between the transport (HTTP) and the payload (app data). If you mistype the URL of a service endpoint, you get a 404. If you ask the service for a non-existent item, you get an app-specific message (maybe with additional information you can use to solve the problem).
    – user44761
    Commented Dec 17, 2015 at 9:50
  • 3
    If you mistype the URL, you might not even end up at the right server, and then anything could happen.
    – gnasher729
    Commented Dec 17, 2015 at 12:08
  • 2
    This is a nicely nuanced look. I think the issue of HTTP becoming a pseudo-transport layer is the real problem with making a determination. I most often run into this question myself when you have an nginx or apache server proxying a nodejs server, where the proxy has rules already for sending these codes, and the question becomes whether it's appropriate for the backend to conform to the standard. In some of those cases there may be an design reason not to send an error code, since nginx may interpret it as 'backend down.' Commented Dec 17, 2015 at 15:14
  • 11
    I agree. There is nothing wrong with an application-layer error being reported in an HTTP 200 response. The 200 indicates that the HTTP request/response itself was successful, without saying anything about its content or the application-layer semantics being invoked at the time. Commented Dec 18, 2015 at 10:46
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.

2
  • 4
    Actually, classifying that as an error on the API-level at all is a curious decision. Even though the client may, at his discretion, classify it as unexpected on the user-level. Commented Dec 16, 2015 at 21:55
  • 2
    There are many things that have to be factored in. It all depends on the implementation of the API. It's also again, a bit opinion-based and also down to what the API defines as "success" and/or "error". The "success": false-flag is more of a hint to the implementer that something's up. Usually it should go with an internal status code. Either "code": "NORESULTS" or a numeric code - whatever the creator of the API fancies. It's mostly there so whoever implements the API can deduct information about what happened on the server.
    – mausworks
    Commented Dec 16, 2015 at 22:07
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.

5
  • 4
    When connecting to an API, I personally would rather get 200 on a 'file not found' when connecting to a valid endpoint, as then my HTTP handling doesn't have to bleed into the layer that handles the API on top of it. Commented Dec 16, 2015 at 22:20
  • 6
    "Missing parameter x" should be a 400 BAD_REQUEST because it is the client doing something wrong. 500 INTERNAL_SERVER_ERROR should be reserved for cases where the server is doing the wrong thing. A 500 implies that the client might be able to try again. A 400 implies that someone should go fix the client.
    – user53141
    Commented Dec 16, 2015 at 22:53
  • 1
    If you are writing a RESTful interface, the URL identifies a specific object and so a 404 is appropriate. It is conceptually the same /customers/premium/johndoe.json refers to a customer that is not in the database and if /files/morefiles/customers.html refers to a page not on the filesystem.
    – user53141
    Commented Dec 16, 2015 at 23:01
  • 1
    @whatsisname What you're saying makes sense because then it's unclear if it's the endpoint that is bad or the resource doesn't exist. You can also argue that whether the endpoint is valid or not, no resource exists at that address so a 404 is correct in both cases.
    – Pete
    Commented Dec 16, 2015 at 23:27
  • 4
    One thing that I haven't seen mentioned is that when you piggyback application errors onto HTTP status codes, you may lose information. If the app just returns a 404 and nothing else, you don't know whether it was because your API returned a 404, or because the server couldn't find the file. That could add one extra step to your debugging. Commented Feb 25, 2016 at 6:33
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.

Not the answer you're looking for? Browse other questions tagged or ask your own question.