Rest error body

Good error message, bad error message Error messages are like letters from the tax...

Good error message, bad error message

Error messages are like letters from the tax authorities. You’d rather not get them, but when you do, you’d prefer them to be clear about what they want you to do next.

When integrating a new API it is inevitable that you’ll encounter an error at some point in your journey. Even if you’re following the docs to the letter and copy & paste code samples, there’s always something that will break – especially if you’ve moved beyond the examples and are now adapting them to fit your use case.

Good error messages are an underrated and underappreciated part of APIs. I would argue that they are just as important a learning path as documentation or examples in teaching developers how to use your API.

As an example, there are many people out there who prefer kinesthetic learning, or learning by doing. They forgo the official docs and prefer to just hack away at their integration armed with an IDE and an API reference.

Let’s start by showing an example of a real error message I’ve seen in the wild:

{
  status: 200,
  body: {
    message: "Error"
  }
}

Enter fullscreen mode

Exit fullscreen mode

If it seems underwhelming, that’s because it is. There are many things that make this error message absolutely unhelpful; let’s go through them one by one.

Send the right code

The above is an error, or is it? The body message says it is, however the status code is 200, which would indicate that everything’s fine. This is not only confusing, but outright dangerous. Most error monitoring systems first filter based on status code and then try to parse the body. This error would likely be put in the “everything’s fine” bucket and get completely missed. Only if you add some natural language processing could you automatically detect that this is in fact an error, which is a ridiculously overengineered solution to a simple problem.

Status codes are for machines, error messages are for humans. While it’s always a good idea to have a solid understanding of status codes, you don’t need to know all of them, especially since some are a bit esoteric. In practise this table is all a user of your API should need to know:

Code Message
200 — 299 All good
400 — 499 You messed up
500 — 599 We messed up

You of course can and should get more specific with the error codes (like a 429 should be sent when you are rate limiting someone for sending too many requests in a short period of time).

The point is that HTTP response status codes are part of the spec for a reason, and you should always make sure you’re sending back the correct code.

This might seem obvious, but it’s easy to accidentally forget status codes, like in this Node example using Express.js:

// ❌ Don't forget the error status code
app.post('/your-api-route', async (req, res) => {      
  try {
    // ... your server logic
  } catch (error) {    
    return res.send({ error: { message: error.message } });
  }  

  return res.send('ok');
});

// ✅ Do set the status correctly
app.post('/your-api-route', async (req, res) => {      
  try {
    // ... your server logic
  } catch (error) {    
    return res.status(400).send({ error: { message: error.message } });
  }  

  return res.send('ok');
});

Enter fullscreen mode

Exit fullscreen mode

In the top snippet we send a 200 status code, regardless of whether an error occurred or not. In the bottom we fix this by simply making sure that we send the appropriate status along with the error message. Note that in production code we’d want to differentiate between a 400 and 500 error, not just a blanket 400 for all errors.

Be descriptive

Next up is the error message itself. I think most people can agree that “Error” is just about as useful as not having a message at all. The status code of the response should already tell you if an error happened or not, the message needs to elaborate so you can actually fix the problem.

It might be tempting to have deliberately obtuse messages as a way of obscuring any details of your inner systems from the end user; however, remember who your audience is. APIs are for developers and they will want to know exactly what went wrong. It’s up to these developers to display an error message, if any, to the end user. Getting an “An error occurred” message can be acceptable if you’re the end user yourself since you’re not the one expected to debug the problem (although it’s still frustrating). As a developer there’s nothing more frustrating than something breaking and the API not having the common decency to tell you what broke.

Let’s take that earlier example of a bad error message and make it better:

{
  status: 404,
  body: {
    error: {
      message: "Customer not found"
    }    
  }
}

Enter fullscreen mode

Exit fullscreen mode

Already we can see:

  • We have a relevant status code: 404, resource not found
  • The message is clear: this was a request that tried to retrieve a customer, and it failed because the customer could not be found
  • The error message is wrapped in an error object, making working with the error a little easier. If not relying on status codes, you could simply check for the existence of body.error to see if an error occurred.

That’s better, but there’s room for improvement here. The error is functional but not helpful.

Be helpful

This is where I think great APIs distinguish themselves from simply “okay” APIs. Letting you know what the error was is the bare minimum, but what a developer really wants to know is how to fix it. A “helpful” API wants to work with the developer by removing any barriers or obstacles to solving the problem.

The message “Customer not found” gives us some clues as to what went wrong, but as API designers we know that we could be giving so much more information here. For starters, let’s be explicit about which customer was not found:

{
  status: 404,
  body: {
    error: {
      message: "Customer cus_Jop8JpEFz1lsCL not found"
    }    
  }
}

Enter fullscreen mode

Exit fullscreen mode

Now not only do we know that there’s an error, but we get the incorrect ID thrown back at us. This is particularly useful when looking through a series of error logs as it tells us whether the problem was with one specific ID or with multiples. This provides clues on whether it’s a problem with a singular customer or with the code that makes the request. Furthermore, the ID has a prefix, so we can immediately tell if it was a case of using the wrong ID type.

We can go further with being helpful. On the API side we have access to information that could be beneficial in solving the error. We could wait for the developer to try and figure it out themselves, or we could just provide them with additional information that we know will be useful.

For instance, in our “Customer not found” example, it’s possible that the reason the customer was not found is because the customer ID provided exists in live mode, but we’re using test mode keys. Using the wrong API keys is an easy mistake to make and is trivial to solve once you know that’s the problem. If on the API side we did a quick lookup to see if the customer object the ID refers to exists in live mode, we could immediately provide that information:

{
  status: 404,
  body: {
    error: {
      message: "Customer cus_Jop8JpEFz1lsCL not found; a similar object exists in live mode, but a test mode key was used to make this request."
    }    
  }
}

Enter fullscreen mode

Exit fullscreen mode

This is much more helpful than what we had before. It immediately identifies the problem and gives you a clue on how to solve it. Other examples of this technique are:

  • In the case of a type mismatch, state what was expected and what was received (“Expected a string, got an integer”)
  • Is the request missing permissions? Tell them how to get them (“Activate this payment method on your dashboard with this URL”)
  • Is the request missing a field? State exactly which one is missing, perhaps linking to the relevant page in your docs or API reference

Note: Be careful with what information you provide in situations like that last bullet point, as it’s possible to leak information that could be a security risk. In the case of an authentication API where you provide a username and password in your request, returning an “incorrect password” error lets a would-be attacker know that while the password isn’t correct, the username is.

Provide more pieces of the puzzle

We can and should strive to be as helpful as possible, but sometimes it isn’t enough. You’ve likely encountered the situation where you thought you were passing in the right fields in your API request, but the API disagrees with you. The easiest way to get to a solution is to look back at the original request and what exactly you passed in. If a developer doesn’t have some sort of logging setup then this is tricky to do, however an API service should always have logs of requests and responses, so why not share that with the developer?

At Stripe we provide a request ID with every response, which can easily be identified as it always starts with req_. Taking this ID and looking it up on the Dashboard gets you a page that details both the request and the response, with extra details to help you debug.

Helpful information on the Stripe Dashboard

Note how the Dashboard also provides the timestamp, API version and even the source (in this case version 8.165 of stripe-node).

As an extra bonus, providing a request ID makes it extremely easy for Stripe engineers in our Discord server to look up your request and help you debug by looking up the request on Stripe’s end.

Be empathetic

The most frustrating error is the 500 error. It means that something went wrong on the API side and therefore wasn’t the developer’s fault. These types of errors could be a momentary glitch or a potential outage on the API provider’s end, which you have no real way of knowing at the time. If the end user relies on your API for a business critical path, then getting these types of errors are very worrying, particularly if you start to get them in rapid succession.

Unlike with other errors, full transparency isn’t as desired here. You don’t want to just dump whatever internal error caused the 500 into the response, as that would reveal sensitive information about the inner workings of your systems. You should be fully transparent about what the user did to cause an error, but you need to be careful what you share when you cause an error.

Like with the first example way up top, a lacklustre “500: error” message is just as useful as not having a message at all. Instead you can put developers at ease by being empathetic and making sure they know that the error has been acknowledged and that someone is looking at it. Some examples:

  • “An error occurred, the team has been informed. If this keeps happening please contact us at {URL}
  • “Something went wrong, please check our status page at {URL} if this keeps happening”
  • “Something goofed, our engineers have been informed. Please try again in a few moments”

It doesn’t solve the underlying problem, but it does help to soften the blow by letting your user know that you’re on it and that they have options to follow up if the error persists.

Putting it all together

In conclusion, a valuable error message should:

  • Use the correct status codes
  • Be descriptive
  • Be helpful
  • Provide elaborative information
  • Be empathetic

Here’s an example of a Stripe API error response after trying to retrieve a customer with the wrong API keys:

{
  status: 404,
  body: {
    error: {
      code: "resource_missing",
      doc_url: "https://stripe.com/docs/error-codes/resource-missing",
      message: "No such customer: 'cus_Jop8JpEFz1lsCL'; a similar object exists in live mode, but a test mode key was used to make this request.",
      param: "id",
      type: "invalid_request_error"
    }
  },
  headers: {    
    'request-id': 'req_su1OkwzKIeEoCy',
    'stripe-version': '2020-08-27',    
  }  
}

Enter fullscreen mode

Exit fullscreen mode

(some headers omitted for brevity)

Here we are:

  1. Using the correct HTTP status code
  2. Wrapping the error in an “error” object
  3. Being helpful by providing:
    1. The error code
    2. The error type
    3. A link to the relevant docs
    4. The API version used in this request
    5. A suggestion on how to fix the issue
  4. Providing the request ID to look up the request and response pairing

The result is an error message so overflowing with useful information that even the most junior of developers will be able to fix the issue and discover how to use the available tools to debug their code themselves.

Designing APIs for humans

By putting all these pieces together we not only provide a way for developers to correct mistakes, but also ensure a powerful way of teaching developers how to use our API. Designing APIs with the human developer in mind means we take steps to make sure that our API isn’t just intuitive, but easy to work with as well.

We covered a lot here and it might seem overwhelming to implement some of these mitigations, however luckily there are some resources out there that can help you make your API human-friendly:

  • The excellent APIs you won’t hate community (co-run by my colleague Mike Bifulco) has some great articles on the subject:
    • Why Show Users Garbage API Errors?
    • Creating Good API errors in REST, GraphQL and gRPC
  • Tools like Spectral can be set up to provide useful linting for APIs — catching things like “200 OK — Error” and making sure best practices are adhered to

Got any examples of error messages you thought were excellent (or terrible, because those are more fun)? I’d love to see them! Drop a comment below or reach out on Twitter.

About the author

Paul Asjes

Paul Asjes is a Developer Advocate at Stripe where he writes, codes and hosts a monthly Q&A series talking to developers. Outside of work he enjoys brewing beer, making biltong and losing to his son in Mario Kart.

  • Do care about your clients
  • Forget your platform: It’s an Integration Style
  • Use HTTP status codes until you bleed!
  • Not enough information?
  • Be more expressive!
  • How about validation errors?
  • “Talk is cheap, show me the code”

Do care about your clients


We all do”. The well known first reaction of all software developers when they first come to this sentence. But,
of course, not when we’re going to handle errors.
We, as software developers, like to only consider happy paths in our scenarios and consequently, tend to forget the fact
that Error Happens, even more than those ordinary happy cases. Honestly, I can’t even remember when was the last time I got an ok
response from a payment API, those are fail by default.
How did you handle errors while designing and implementing your very last REST API? How did you handle validation errors? How did you deal with uncaught exceptions in your services? If you didn’t answer these questions before, now it’s time to consider answering them before designing a new REST API.
Of course, The most well known and unfortunate approach is let the damn exception propagates until our beloved client sees the
beautiful stacktrace on her client! If she can’t handle our NullPointerException, she shouldn’t call herself a developer.

Forget your platform: It’s an Integration Style


In their amazing book, Enterprise Integration Patterns, Gregor Hohpe and Bobby Woolf described one of the most important aspects of applications as the following:

Interesting applications rarely live in isolation. Whether your sales application must interface
with your inventory application, your procurement application must connect to an auction site,
or your PDA’s PIM must synchronize with the corporate calendar server, it seems like any
application can be made better by integrating it with other applications.

Then they introduced four Integration Styles. Regardless of the fact that the book was mainly about Messaging Patterns, we’re going to focus on Remote Procedure Invocation. Remote Procedure Invocation is an umbrella term for all approaches that expose some of application
functionalities through a Remote Interface, like REST and RPC.
Now that we’ve established that REST is an Integration Style:

Do Not expose your platform specific conventions into the REST API

The whole point of using an Integration Style, like REST, is to let different applications developed in different platforms work together,
hopefully in a more seamless and less painful fashion. The last thing a python client developer wanna see is a bunch of Pascal Case URLs, just because the API was developed with C#. Don’t shove all those Beans into your message bodies just because you’re using Java.
For a moment, forget about the platform and strive to provide an API aligned with mainstream approaches in REST API design, like the
conventions provided by Zalando team.

Use HTTP status codes until you bleed!


Let’s start with a simple rule:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 31 Aug 2016 20:51:25 GMT

{"status": "failed"}

Never ever convey error information in 2xx and 3xx response bodies!

Many so-called RESTful services out there are using HTTP status codes incorrectly. One fundamentally wrong approach is to convey
error information through a 200 OK response. The other common and irresponsible approach is to return the stacktrace as part of 500 Internal Server Error body:

HTTP/1.1 500 Internal Server Error
Content-Type: text/plain
Content-Length: 209
Date: Sun, 18 Sep 2016 09:03:48 GMT

JAXBException occurred : unexpected element (uri:"", local:"LoginRequests").
Expected elements are <{}LoginRequest>. unexpected element
(uri:"", local:"LoginRequests"). Expected elements are <{}LoginRequest>.

Here is another simple rule to follow unconditionally:

Say goodbye to your stacktrace!

There is a handful of error status codes available on 4xx and 5xx ranges that you can use to signal client or server errors. For example, suppose we have a Geeks API and we can create a new geek using POST on /geeks endpoint. If client didn’t send the required parameters,
we can tell her about the error using a 400 Bad Request response. If authentication is required but client did not send any authentication parameters in her request, we can respond with a 401 Unauthorized response.
Other typical error responses may use 403 Forbidden, 404 Not Found, 429 Too Many Requests, 500 Internal Server Error and 503 Service Unavailable. Of course these are not all available status codes to use, for an exhaustive list of them, consider reading this Wikipedia entry.

Not enough information?


Even when we’re using HTTP status codes to indicate client or server errors, there may be some cases that we need to provide more information about the error. Sometimes we use the same status code to respond for multiple error cases. For example, the POST /geeks endpoint may return 400 Bad Request for both validation errors and when that geek already exists. In such scenarios, How client would be able to tell those errors apart?
One common solution for this problem is to use Application Level Error Codes. These are codes you define to indicate one and only one error case. For example, you may reserve error code 42 to say the geek already exists:

{
  "status_code": 400,
  "reason_phrase": "Bad Request",
  "error_code": 42
}

status_code and reason_phrase fields would come handy when someone is testing the API via browser. Anyway, clients can find out what exactly went wrong just by inspecting the error_code field.
Using a numeric error code is the most common approach to implement application level error codes. However, selecting the error code is a one of the main challenges of this approach. If we assign our error codes with no strategy and upfront thinking whatsoever, we may end up with a error code mess. One solution to this challenge is to define Ranges of error codes. For example, if our fictional REST service has two resources, we can define three ranges like the following:

  • 1 to 50 for general error codes, e.g. 1 for invalid JSON payload
  • 51 to 100 for Geeks API (under /geeks), e.g. 51 for geek already exist
  • 101 to 150 for Technologies API (under /technologies)

Since different resources aren’t equally error prone, we may assign different range sizes to different resources. Estimating each range size is the other challenge we should thought through while designing the API.
The other approach which I tend to prefer to the numeric error codes is Resource Based Error Codes. That is, we use a string prefix for each error code and that prefix is determined by the resource itself. For example:

  • All general errors are prefixed with gen-, e.g. gen-1 for invalid JSON payload
  • Geeks API errors are prefixed with geeks-, e.g. geeks-1 for geek already exists
  • Technologies API errors are prefixed with techs-, e.g. techs-1 for whatever

This way our error codes are a little more verbose but wouldn’t have those mentioned challenges:

{
  "status_code": 400,
  "reason_phrase": "Bad Request",
  "error_code": "geeks-1"
}

Be more expressive!


What is your first reaction when you see the following error response?

{
  "status_code": 400,
  "reason_phrase": "Bad Request",
  "error_code": "geeks-1"
}

I bet you would search the API documentation (if any!) like a clueless chicken to find out what the heck this geeks-1 code means? Clients would have a nicer API experience if we provide an error message corresponding to the error code:

{
  "status_code": 400,
  "reason_phrase": "Bad Request",
  "error_code": "geeks-1",
  "error_message": "The geek already exists"
}

If you live in a multilingual context, you could go one step further by changing the error_message language through Content Negotiation process. For example, if the client set the Accept-Language header to fr-FR, the error could be (Google Translate says so):

{
  "status_code": 400,
  "reason_phrase": "Bad Request",
  "error_code": "geeks-1",
  "error_message": "Le connaisseur existe déjà"
}

How about validation errors?


There is this possibility that client has more than one validation error in her request. With our current error response schema, we can only report one error with each http response. That is, in multiple validation errors scenario, first we complain about the first validation error, then the second error and so on. If we could report all validation errors at once, in addition to have a more pleasant API, we could save some extra round trips.
Anyway, we can refactor our error response model to return an array of errors (surprise!) instead of just one error:

{
  "status_code": 400,
  "reason_phrase": "Bad Request",
  "errors": [
      {"code": "geeks-2", "message": "The first_name is mandatory"},
      {"code": "geeks-3", "message": "The last_name is mandatory"}
  ]
}

“Talk is cheap, show me the code”


Here I’m gonna provide a very minimal implementation of what we’ve talked about so far with Spring Boot. All the following codes are available at github, you can skip rest of the article and check them out now. Anyway, Let’s start with error codes, The ErrorCode interface will act as a super-type for all our error codes:

/**
 * Represents API error code. Each API should implement this interface to
 * provide an error code for each error case.
 *
 * @implNote Enum implementations are good fit for this scenario.
 *
 * @author Ali Dehghani
 */
public interface ErrorCode {
    String ERROR_CODE_FOR_UNKNOWN_ERROR = "unknown";

    /**
     * Represents the error code.
     *
     * @return The resource based error code
     */
    String code();

    /**
     * The corresponding HTTP status for the given error code
     *
     * @return Corresponding HTTP status code, e.g. 400 Bad Request for a validation
     * error code
     */
    HttpStatus httpStatus();

    /**
     * Default implementation representing the Unknown Error Code. When the
     * {@linkplain ErrorCodes} couldn't find any appropriate
     * {@linkplain ErrorCode} for any given {@linkplain Exception}, it will
     * use this implementation by default.
     */
    enum UnknownErrorCode implements ErrorCode {
        INSTANCE;

        @Override
        public String code() {
            return ERROR_CODE_FOR_UNKNOWN_ERROR;
        }

        @Override
        public HttpStatus httpStatus() {
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }
    }
}

Each error code has a String based code and a HttpStatus corresponding to that code. There is a default implementation of this interface embedded into the interface itself called UnknownErrorCode. This implementation will be used when we couldn’t figure out what exactly went wrong in the server, which obviously is a server error.
As I said each resource should provide an implementation of this interface to define all its possible error codes. For example, Geeks API did that like the following:

enum GeeksApiErrorCodes implements ErrorCode {
    GEEK_ALREADY_EXISTS("geeks-1", HttpStatus.BAD_REQUEST);

    private final String code;
    private final HttpStatus httpStatus;

    GeeksApiErrorCodes(String code, HttpStatus httpStatus) {
        this.code = code;
        this.httpStatus = httpStatus;
    }

    @Override
    public String code() {
        return code;
    }

    @Override
    public HttpStatus httpStatus() {
        return httpStatus;
    }
}

Then we should somehow convert different exceptions to appropriate ErrorCodes. ExceptionToErrorCode strategy interface does that for us:

/**
 * Strategy interface responsible for converting an instance of
 * {@linkplain Exception} to appropriate instance of {@linkplain ErrorCode}.
 * Implementations of this interface should be accessed indirectly through
 * the {@linkplain ErrorCodes}'s factory method.
 *
 * @implSpec The {@linkplain #toErrorCode(Exception)} method should be called iff the
 * call to the {@linkplain #canHandle(Exception)} returns true for the same exception.
 *
 * @see ErrorCode
 * @see ErrorCodes
 *
 * @author Ali Dehghani
 */
public interface ExceptionToErrorCode {
    /**
     * Determines whether this implementation can handle the given exception or not.
     * Calling the {@linkplain #toErrorCode(Exception)} when this method returns
     * false, is strongly discouraged.
     *
     * @param exception The exception to examine
     * @return true if the implementation can handle the exception, false otherwise.
     */
    boolean canHandle(Exception exception);

    /**
     * Performs the actual mechanics of converting the given {@code exception}
     * to an instance of {@linkplain ErrorCode}.
     *
     * @implSpec Call this method iff the {@linkplain #canHandle(Exception)} returns
     * true.
     *
     * @param exception The exception to convert
     * @return An instance of {@linkplain ErrorCode} corresponding to the given
     * {@code exception}
     */
    ErrorCode toErrorCode(Exception exception);
}

Again each resource should provide an implementation for this interface per each defined error codes. Geeks API does that like the following:

class GeeksApiExceptionMappers {
    @Component
    static class GeeksAlreadyExceptionToErrorCode implements ExceptionToErrorCode {
        @Override
        public boolean canHandle(Exception exception) {
            return exception instanceof GeekAlreadyExists;
        }

        @Override
        public ErrorCode toErrorCode(Exception exception) {
            return GeeksApiErrorCodes.GEEK_ALREADY_EXISTS;
        }
    }
}

This basically catch all GeekAlreadyExists exceptions and convert them to GeeksApiErrorCodes.GEEK_ALREADY_EXISTS error code.
Since there would be quite a large number of implementations like this, It’s better to delegate finding the right implementation for each exception to another abstraction. ErrorCodes factory would do that like the following:

/**
 * Acts as a factory for {@linkplain ExceptionToErrorCode} implementations.
 * By calling the {@linkplain #of(Exception)} factory method, clients can
 * find the corresponding {@linkplain ErrorCode} for the given
 * {@linkplain Exception}. Behind the scenes, this factory method, first finds
 * all available implementations of the {@linkplain ExceptionToErrorCode}
 * strategy interface. Then selects the first implementation that can actually
 * handles the given exception and delegate the exception translation process
 * to that implementation.
 *
 * @implNote If the factory method couldn't find any implementation for the given
 * {@linkplain Exception}, It would return the
 * {@linkplain me.alidg.rest.errors.ErrorCode.UnknownErrorCode} which represent
 * an Unknown Error.
 *
 * @author Ali Dehghani
 */
@Component
class ErrorCodes {
    private final ApplicationContext context;

    ErrorCodes(ApplicationContext context) {
        this.context = context;
    }

    /**
     * Factory method to find the right {@linkplain ExceptionToErrorCode}
     * implementation and delegates the conversion task to that implementation.
     * If it couldn't find any registered implementation, It would return an
     * Unknown Error represented by the {@linkplain UnknownError} implementation.
     *
     * @implNote Currently this method queries Spring's
     * {@linkplain ApplicationContext} to find the {@linkplain ExceptionToErrorCode}
     * implementations. So in order to register your {@linkplain ExceptionToErrorCode}
     * implementation, you should annotate your implementation with one of
     * Spring Stereotype annotations, e.g. {@linkplain Component}.
     * Our recommendation is to use the {@linkplain Component} annotation or
     * another meta annotation based on this annotation.
     *
     * @param exception The exception to find the implementation based on that
     * @return An instance of {@linkplain ErrorCode} corresponding the given
     * {@code exception}
     */
    ErrorCode of(Exception exception) {
        return implementations()
                .filter(impl -> impl.canHandle(exception))
                .findFirst()
                .map(impl -> impl.toErrorCode(exception))
                .orElse(ErrorCode.UnknownErrorCode.INSTANCE);
    }

    /**
     * Query the {@linkplain #context} to find all available implementations of
     * {@linkplain ExceptionToErrorCode}.
     */
    private Stream<ExceptionToErrorCode> implementations() {
        return context.getBeansOfType(ExceptionToErrorCode.class).values().stream();
    }
}

Here comes the ErrorResponse class to used to as the JSON representation of each error:

/**
 * An immutable data structure representing HTTP error response bodies. JSON
 * representation of this class would be something like the following:
 * <pre>
 *     {
 *         "status_code": 404,
 *         "reason_phrase": "Not Found",
 *         "errors": [
 *             {"code": "res-15", "message": "some error message"},
 *             {"code": "res-16", "message": "yet another message"}
 *         ]
 *     }
 * </pre>
 *
 * @author Ali Dehghani
 */
@JsonAutoDetect(fieldVisibility = ANY)
class ErrorResponse {
    /**
     * The 4xx or 5xx status code for error cases, e.g. 404
     */
    private final int statusCode;

    /**
     * The HTTP reason phrase corresponding the {@linkplain #statusCode},
     * e.g. Not Found
     *
     * @see <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html">
     * Status Code and Reason Phrase</a>
     */
    private final String reasonPhrase;

    /**
     * List of application-level error code and message combinations.
     * Using these errors we provide more information about the
     * actual error
     */
    private final List<ApiError> errors;

    private ErrorResponse(int statusCode, String reason, List<ApiError> errors) {
        // Some precondition checks

        this.statusCode = statusCode;
        this.reasonPhrase = reason;
        this.errors = errors;
    }

    /**
     * Static factory method to create a {@linkplain ErrorResponse} with multiple
     * {@linkplain ApiError}s. The canonical use case of this factory method is when
     * we're handling validation exceptions, since we may have multiple validation
     * errors.
     */
    static ErrorResponse ofErrors(HttpStatus status, List<ApiError> errors) {
        return new ErrorResponse(status.value(), status.getReasonPhrase(), errors);
    }

    /**
     * Static factory method to create a {@linkplain ErrorResponse} with a single
     * {@linkplain ApiError}. The canonical use case for this method is when we trying
     * to create {@linkplain ErrorResponse}es for regular non-validation exceptions.
     */
    static ErrorResponse of(HttpStatus status, ApiError error) {
        return ofErrors(status, Collections.singletonList(error));
    }

    /**
     * An immutable data structure representing each application-level error. JSON
     * representation of this class would be something like the following:
     * <pre>
     *     {"code": "res-12", "message": "some error"}
     * </pre>
     *
     * @author Ali Dehghani
     */
    @JsonAutoDetect(fieldVisibility = ANY)
    static class ApiError {
        /**
         * The error code
         */
        private final String code;

        /**
         * Possibly localized error message
         */
        private final String message;

        ApiError(String code, String message) {
            this.code = code;
            this.message = message;
        }
    }
}

And finally to glue all of these together, a Spring MVC ExceptionHandler would catch all exceptions and render appropriate ErrorResponsees:

/**
 * Exception handler that catches all exceptions thrown by the REST layer
 * and convert them to the appropriate {@linkplain ErrorResponse}s with a
 * suitable HTTP status code.
 *
 * @see ErrorCode
 * @see ErrorCodes
 * @see ErrorResponse
 *
 * @author Ali Dehghani
 */
@ControllerAdvice
class ApiExceptionHandler {
    private static final String NO_MESSAGE_AVAILABLE = "No message available";

    /**
     * Factory to convert the given {@linkplain Exception} to an instance of
     * {@linkplain ErrorCode}
     */
    private final ErrorCodes errorCodes;

    /**
     * Responsible for finding the appropriate error message(s) based on the given
     * {@linkplain ErrorCode} and {@linkplain Locale}
     */
    private final MessageSource apiErrorMessageSource;

    /**
     * Construct a valid instance of the exception handler
     *
     * @throws NullPointerException If either of required parameters were {@code null}
     */
    ApiExceptionHandler(ErrorCodes errorCodes, MessageSource apiErrorMessageSource) {
        Objects.requireNonNull(errorCodes);
        Objects.requireNonNull(apiErrorMessageSource);

        this.errorCodes = errorCodes;
        this.apiErrorMessageSource = apiErrorMessageSource;
    }

    /**
     * Catches all non-validation exceptions and tries to convert them to
     * appropriate HTTP Error responses
     *
     * <p>First using the {@linkplain #errorCodes} will find the corresponding
     * {@linkplain ErrorCode} for the given {@code exception}. Then based on
     * the resolved {@linkplain Locale}, a suitable instance of
     * {@linkplain ErrorResponse} with appropriate and localized message will
     * return to the client. {@linkplain ErrorCode} itself determines the HTTP
     * status of the response.
     *
     * @param exception The exception to convert
     * @param locale The locale that usually resolved by {@code Accept-Language}
     * header. This locale will determine the language of the returned error
     * message.
     * @return An appropriate HTTP Error Response with suitable status code
     * and error messages
     */
    @ExceptionHandler(ServiceException.class)
    ResponseEntity<ErrorResponse> handleExceptions(ServiceException exception,
                                                   Locale locale) {
        ErrorCode errorCode = errorCodes.of(exception);
        ErrorResponse errorResponse = ErrorResponse.of(errorCode.httpStatus(),
                                                       toApiError(errorCode, locale));

        return ResponseEntity.status(errorCode.httpStatus()).body(errorResponse);
    }

    /**
     * Convert the passed {@code errorCode} to an instance of
     * {@linkplain ErrorResponse} using the given {@code locale}
     */
    private ErrorResponse.ApiError toApiError(ErrorCode errorCode, Locale locale) {
        String message;
        try {
            message = apiErrorMessageSource.getMessage(errorCode.code(),
                                                       new Object[]{},
                                                       locale);
        } catch (NoSuchMessageException e) {
            message = NO_MESSAGE_AVAILABLE;
        }

        return new ErrorResponse.ApiError(errorCode.code(), message);
    }
}

Время прочтения
6 мин

Просмотры 13K

Почти все разработчики так или иначе постоянно работают с api по http, клиентские разработчики работают с api backend своего сайта или приложения, а бэкендеры «дергают» бэкенды других сервисов, как внутренних, так и внешних. И мне кажется, одна из самых главных вещей в хорошем API это формат передачи ошибок. Ведь если это сделано плохо/неудобно, то разработчик, использующий это API, скорее всего не обработает ошибки, а клиенты будут пользоваться молчаливо ломающимся продуктом.

За 7 лет я как поддерживал множество legacy API, так и разрабатывал c нуля. И я поработал, наверное, с большинством стратегий по возвращению ошибок, но каждая из них создавала дискомфорт в той или иной мере. В последнее время я нащупал оптимальный вариант, о котором и хочу рассказать, но с начала расскажу о двух наиболее популярных вариантах.

№1: HTTP статусы

Если почитать апологетов REST, то для кодов ошибок надо использовать HTTP статусы, а текст ошибки отдавать в теле или в специальном заголовке. Например:

Success:

HTTP 200 GET /v1/user/1
Body: { name: 'Вася' }

Error:

HTTP 404 GET /v1/user/1
Body: 'Не найден пользователь'

Если у вас примитивная бизнес-логика или API из 5 url, то в принципе это нормальный подход. Однако как-только бизнес-логика станет сложнее, то начнется ряд проблем.

Http статусы предназначались для описания ошибок при передаче данных, а про логику вашего приложения никто не думал. Статусов явно не хватает для описания всего разнообразия ошибок в вашем проекте, да они и не были для этого предназначены. И тут начинается натягивание «совы на глобус»: все начинают спорить, какой статус ошибки дать в том или ином случае. Пример: Есть API для task manager. Какой статус надо вернуть в случае, если пользователь хочет взять задачу, а ее уже взял в работу другой пользователь? Ссылка на http статусы. И таких проблемных примеров можно придумать много.

REST скорее концепция, чем формат общения из чего следует неоднозначность использования статусов. Разработчики используют статусы как им заблагорассудится. Например, некоторые API при отсутствии сущности возвращают 404 и текст ошибки, а некоторые 200 и пустое тело.

Бэкенд разработчику в проекте непросто выбрать статус для ошибки, а клиентскому разработчику неочевидно какой статус предназначен для того или иного типа ошибок бизнес-логики. По-хорошему в проекте придется держать enum для того, чтобы описать какие ошибки относятся к тому или иному статусу.

Когда бизнес-логика приложения усложняется, начинают делать как-то так:

HTTP 400 PUT /v1/task/1 { status: 'doing' }
Body: { error_code: '12', error_message: 'Задача уже взята другим исполнителем' } 

Из-за ограниченности http статусов разработчики начинают вводить “свои” коды ошибок для каждого статуса и передавать их в теле ответа. Другими словами, пользователю API приходится писать нечто подобное:

if (status === 200) {
  // Success
} else if (status === 500) {
  // some code
} else if (status === 400) {
  if (body.error_code === 1) {
    // some code
  } else if (body.error_code === 2) {
    // some code
  } else {
    // some code
  }
} else if (status === 404) {
  // some code
} else {
  // some code
}

Из-за этого ветвление клиентского кода начинает стремительно расти: множество http статусов и множество кодов в самом сообщении. Для каждого ошибочного http статуса необходимо проверить наличие кодов ошибок в теле сообщения. От комбинаторного взрыва начинает конкретно пухнуть башка! А значит обработку ошибок скорее всего сведут к сообщению типа “Произошла ошибка” или к молчаливому некорректному поведению.

Многие системы мониторинга сервисов привязываются к http статусам, но это не помогает в мониторинге, если статусы используются для описания ошибок бизнес логики. Например, у нас резкий всплеск ошибок 429 на графике.  Это началась DDOS атака, или кто-то из разработчиков выбрал неудачный статус?

Итог:  Начать с таким подходом легко и просто и для простого API это вполне подойдет. Но если логика стала сложнее, то использование статусов для описания того, что не укладывается в заданные рамки протокола http приводит к неоднозначности использования и последующим костылям для работы с ошибками. Или что еще хуже к формализму, что ведет к неприятному пользовательскому опыту.

№2: На все 200

Есть другой подход, даже более старый, чем REST, а именно: на все ошибки связанные с бизнес-логикой возвращать 200, а уже в теле ответа есть информация об ошибке. Например:

Вариант 1:

Success:
HTTP 200 GET /v1/user/1
Body: { ok: true, data: { name: 'Вася' } }

Error:
HTTP 200 GET /v1/user/1
Body: { ok: false, error: { code: 1, msg: 'Не найден пользователь' } }

Вариант 2:

Success:
HTTP 200 GET /v1/user/1
Body: { data: { name: 'Вася' }, error: null }

Error:
HTTP 200 GET /v1/user/1
Body: { data: null, error: { code: 1, msg: 'Не найден пользователь' } }

На самом деле формат зависит от вас или от выбранной библиотеки для реализации коммуникации, например JSON-API.

Звучит здорово, мы теперь отвязались от http статусов и можем спокойно ввести свои коды ошибок. У нас больше нет проблемы “впихнуть невпихуемое”. Выбор нового типа ошибки не вызывает споров, а сводится просто к введению нового числового номера (например, последовательно) или строковой константы. Например:

module.exports = {
  NOT_FOUND: 1,
  VALIDATION: 2,
 // ….
}

module.exports = {
  NOT_FOUND: ‘NOT_AUTHORIZED’,
  VALIDATION: ‘VALIDATION’,
 // ….
}

Клиентские разработчики просто основываясь на кодах ошибок могут создать классы/типы ошибок и притом не бояться, что сервер вернет один и тот же код для разных типов ошибок (из-за бедности http статусов).

Обработка ошибок становится менее ветвящейся, множество http статусов превратились в два: 200 и все остальные (ошибки транспорта).

if (status === 200) {
  if (body.error) {
    var error = body.error;
    if (error.code === 1) {
      // some code
    } else if (error.code === 2) {
      // some code
    } else {
      // some code
    }
  } else {
    // Success
  }
} else {
  // transport erros
}

В некоторых случаях, если есть библиотека десериализации данных, она может взять часть работы на себя. Писать SDK вокруг такого подхода проще нежели вокруг той или иной имплементации REST, ведь реализация зависит от того, как это видел автор. Кроме того, теперь никто не вызовет случайное срабатывание alert в мониторинге из-за того, что выбрал неудачный код ошибки.

Но неудобства тоже есть:

  • Избыточность полей при передаче данных, т.е. нужно всегда передавать 2 поля: для данных и для ошибки. Это усложняет чтение логов и написание документации.

  • При использовании средств отладки (Chrome DevTools) или других подобных инструментов вы не сможете быстро найти ошибочные запросы бизнес логики, придется обязательно заглянуть в тело ответа (ведь всегда 200)

  • Мониторинг теперь точно будет срабатывать только на ошибки транспорта, а не бизнес-логики, но для мониторинга логики надо будет дописывать парсинг тела сообщения.

В некоторых случаях данный подход вырождается в RPC, то есть по сути вообще отказываются от использования url и шлют все на один url методом POST, а в теле сообщения передают все параметры. Мне кажется это не правильным, ведь url это прекрасный именованный namespace, зачем от этого отказываться, не понятно?! Кроме того, RPC создает проблемы:

  • нельзя кэшировать по http GET запросы, так как замешали чтение и запись в один метод POST

  • нельзя делать повторы для неудавшихся GET запросов (на backend) на реверс-прокси (например, nginx) по указанной выше причине

  • имеются проблемы с документированием – swagger и  ApiDoc не подходят, а удобных аналогов я не нашел

Итог: Для сложной бизнес-логики с большим количеством типов ошибок такой подход лучше, чем расплывчатый REST, не зря в проектах c “разухабистой” бизнес-логикой  часто именно такой подход и используют.

№3: Смешанный

Возьмем лучшее от двух миров. Мы выберем один http статус, например, 400 или 422 для всех ошибок бизнес-логики, а в теле ответа будем указывать код ошибки или строковую константу. Например:

Success:

HTTP 200 /v1/user/1
Body: { name: 'Вася' }

Error:

HTTP 400 /v1/user/1
Body: { error: { code: 1, msg: 'Не найден пользователь' } }

Коды:

  • 200 – успех

  • 400 – ошибка бизнес логики

  • остальное ошибки в транспорте

Тело ответа для удачного запроса у нас имеет произвольную структуру, а вот для ошибки есть четкая  схема. Мы избавляемся от избыточности данных (поле ошибки/данных) благодаря использованию http статуса в сравнении со вторым вариантом. Клиентский код упрощается в плане обработки ошибки (в сравнении с первым вариантом). Также мы снижаем его вложенность за счет использования отдельного http статуса для ошибок бизнес логики (в сравнении со вторым вариантом).

if (status === 200) {
  // Success
} else if (status === 400) {
  if (body.error.code === 1) {
    // some code
  } else if (body.error.code === 2) {
    // some code
  } else {
    // some code
  }
} else {
  // transport erros
}

Мы можем расширять объект ошибки для детализации проблемы, если хотим. С мониторингом все как во втором варианте, дописывать парсинг придется, но и риска “стрельбы” некорректными alert нету. Для документирования можем спокойно использовать Swagger и ApiDoc. При этом сохраняется удобство использования инструментов разработчика, таких как Chrome DevTools, Postman, Talend API.

Итог:  Использую данный подход уже в нескольких проектах, где множество типов ошибок и все крайне довольны, как клиентские разработчики, так и бэкендеры. Внедрение новой ошибки не вызывает споров, проблем и противоречий. Данный подход объединяет преимущества первого и второго варианта, при этом код более читабельный и структурированный.

Самое главное какой бы формат ошибок вы бы не выбрали лучше обговорить его заранее и следовать ему. Если эту вещь пустить на “самотек”, то очень скоро обработка ошибок в проекте станет невыносимо сложной для всех.

P.S. Иногда ошибки любят передавать массивом

{ error: [{ code: 1, msg: 'Не найден пользователь' }] }

Но это актуально в основном в двух случаях:

  • Когда наш API выступает в роли сервиса без фронтенда (нет сайта/приложения). Например, сервис платежей.

  • Когда в API есть url для загрузки какого-нибудь длинного отчета в котором может быть ошибка в каждой строке/колонке. И тогда для пользователя удобнее, чтобы ошибки в приложении сразу показывались все, а не по одной.

В противном случае нет особого смысла закладываться сразу на массив ошибок, потому что базовая валидация данных должна происходить на клиенте, зато код упрощается как на сервере, так и на клиенте. А user-experience хакеров, лезущих напрямую  в наше API,  не должен нас волновать?HTTP

Some Background

REST APIs use the Status-Line part of an HTTP response message to inform clients of their request’s overarching result.
RFC 2616 defines the Status-Line syntax as shown below:

Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF

A great amount of applications are using Restful APIs that are based on the HTTP protocol for connecting their clients. In all the calls, the server and the endpoint at the client both return a call status to the client which can be in the form of:

  • The success of API call.
  • Failure of API call.

In both the cases, it is necessary to let the client know so that they can proceed to the next step. In the case of a successful API call they can proceed to the next call or whatever their intent was in the first place but in the case of latter they will be forced to modify their call so that the failed call can be recovered.

RestCase

To enable the best user experience for your customer, it is necessary on the part of the developers to make excellent error messages that can help their client to know what they want to do with the information they get. An excellent error message is precise and lets the user know about the nature of the error so that they can figure their way out of it.

A good error message also allows the developers to get their way out of the failed call.

Next step is to know what error messages to integrate into your framework so that the clients on the end point and the developers at the server are constantly made aware of the situation which they are in. in order to do so, the rule of thumb is to keep the error messages to a minimum and only incorporate those error messages which are helpful.

HTTP defines over 40 standard status codes that can be used to convey the results of a client’s request. The status codes are divided into the five categories presented here:

  • 1xx: Informational — Communicates transfer protocol-level information
  • 2xx: Success -Indicates that the client’s request was accepted successfully.
  • 3xx: Redirection — Indicates that the client must take some additional action in order to complete their request.
  • 4xx: Client Error — This category of error status codes points the finger at clients.
  • 5xx: Server Error — The server takes responsibility for these error status codes.

HTTP & REST

If you would ask me 5 years ago about HTTP Status codes I would guess that the talk is about web sites, status 404 meaning that some page was not found and etc. But today when someone asks me about HTTP Status codes, it is 99.9% refers to REST API web services development. I have lots of experience in both areas (Website development, REST API web services development) and it is sometimes hard to come to a conclusion about what and how use the errors in REST APIs.

There are some cases where this status code is always returned, even if there was an error that occurred. Some believe that returning status codes other than 200 is not good as the client did reach your REST API and got response.

Proper use of the status codes will help with your REST API management and REST API workflow management.

If for example the user asked for “account” and that account was not found there are 2 options to use for returning an error to the user:

  • Return 200 OK Status and in the body return a json containing explanation that the account was not found.

  • Return 404 not found status.
    The first solution opens up a question whether the user should work a bit harder to parse the json received and to see whether that json contains error or not.

  • There is also a third solution: Return 400 Error — Client Error. I will explain a bit later why this is my favorite solution.

It is understandable that for the user it is easier to check the status code of 404 without any parsing work to do.

I my opinion this solution is actually miss-use of the HTTP protocol

We did reach the REST API, we did got response from the REST API, what happens if the users misspells the URL of the REST API – he will get the 404 status but that is returned not by the REST API itself.

I think that these solutions should be interesting to explore and to see the benefits of one versus the other.

There is also one more solution that is basically my favorite – this one is a combination of the first two solutions, he is also gives better Restful API services automatic testing support because only several status codes are returned, I will try to explain about it.

Error handling Overview

Error responses should include a common HTTP status code, message for the developer, message for the end-user (when appropriate), internal error code (corresponding to some specific internally determined ID), links where developers can find more info. For example:

‘{ «status» : 400,
«developerMessage» : «Verbose, plain language description of the problem. Provide developers suggestions about how to solve their problems here»,
«userMessage» : «This is a message that can be passed along to end-users, if needed.»,
«errorCode» : «444444»,
«moreInfo» : «http://www.example.gov/developer/path/to/help/for/444444,
http://tests.org/node/444444»,
}’

How to think about errors in a pragmatic way with REST?
Apigee’s blog post that talks about this issue compares 3 top API providers.

REST API Error Codes

Facebook

No matter what happens on a Facebook request, you get back the 200 status code — everything is OK. Many error messages also push down into the HTTP response. Here they also throw an #803 error but with no information about what #803 is or how to react to it.

Twilio

Twilio does a great job aligning errors with HTTP status codes. Like Facebook, they provide a more granular error message but with a link that takes you to the documentation. Community commenting and discussion on the documentation helps to build a body of information and adds context for developers experiencing these errors.

SimpleGeo

Provides error codes but with no additional value in the payload.

Error Handling — Best Practises

First of all: Use HTTP status codes! but don’t overuse them.
Use HTTP status codes and try to map them cleanly to relevant standard-based codes.
There are over 70 HTTP status codes. However, most developers don’t have all 70 memorized. So if you choose status codes that are not very common you will force application developers away from building their apps and over to wikipedia to figure out what you’re trying to tell them.

Therefore, most API providers use a small subset.
For example, the Google GData API uses only 10 status codes, Netflix uses 9, and Digg, only 8.

REST API Status Codes Subset

How many status codes should you use for your API?

When you boil it down, there are really only 3 outcomes in the interaction between an app and an API:

  • Everything worked
  • The application did something wrong
  • The API did something wrong

Start by using the following 3 codes. If you need more, add them. But you shouldn’t go beyond 8.

  • 200 — OK
  • 400 — Bad Request
  • 500 — Internal Server Error

Please keep in mind the following rules when using these status codes:

200 (OK) must not be used to communicate errors in the response body

Always make proper use of the HTTP response status codes as specified by the rules in this section. In particular, a REST API must not be compromised in an effort to accommodate less sophisticated HTTP clients.

400 (Bad Request) may be used to indicate nonspecific failure

400 is the generic client-side error status, used when no other 4xx error code is appropriate. For errors in the 4xx category, the response body may contain a document describing the client’s error (unless the request method was HEAD).

500 (Internal Server Error) should be used to indicate API malfunction 500 is the generic REST API error response.

Most web frameworks automatically respond with this response status code whenever they execute some request handler code that raises an exception. A 500 error is never the client’s fault and therefore it is reasonable for the client to retry the exact same request that triggered this response, and hope to get a different response.

If you’re not comfortable reducing all your error conditions to these 3, try adding some more but do not go beyond 8:

  • 401 — Unauthorized
  • 403 — Forbidden
  • 404 — Not Found

Please keep in mind the following rules when using these status codes:

A 401 error response indicates that the client tried to operate on a protected resource without providing the proper authorization. It may have provided the wrong credentials or none at all.

403 (Forbidden) should be used to forbid access regardless of authorization state

A 403 error response indicates that the client’s request is formed correctly, but the REST API refuses to honor it. A 403 response is not a case of insufficient client credentials; that would be 401 (“Unauthorized”). REST APIs use 403 to enforce application-level permissions. For example, a client may be authorized to interact with some, but not all of a REST API’s resources. If the client attempts a resource interaction that is outside of its permitted scope, the REST API should respond with 403.

404 (Not Found) must be used when a client’s URI cannot be mapped to a resource

The 404 error status code indicates that the REST API can’t map the client’s URI to a resource.

RestCase

Conclusion

I believe that the best solution to handle errors in a REST API web services is the third option, in short:
Use three simple, common response codes indicating (1) success, (2) failure due to client-side problem, (3) failure due to server-side problem:

  • 200 — OK
  • 400 — Bad Request (Client Error) — A json with error more details should return to the client.
  • 401 — Unauthorized
  • 500 — Internal Server Error — A json with an error should return to the client only when there is no security risk by doing that.

I think that this solution can also ease the client to handle only these 4 status codes and when getting either 400 or 500 code he should take the response message and parse it in order to see what is the problem exactly and on the other hand the REST API service is simple enough.

The decision of choosing which error messages to incorporate and which to leave is based on sheer insight and intuition. For example: if an app and API only has three outcomes which are; everything worked, the application did not work properly and API did not respond properly then you are only concerned with three error codes. By putting in unnecessary codes, you will only distract the users and force them to consult Google, Wikipedia and other websites.

Most important thing in the case of an error code is that it should be descriptive and it should offer two outputs:

  • A plain descriptive sentence explaining the situation in the most precise manner.
  • An ‘if-then’ situation where the user knows what to do with the error message once it is returned in an API call.

The error message returned in the result of the API call should be very descriptive and verbal. A code is preferred by the client who is well versed in the programming and web language but in the case of most clients they find it hard to get the code.

As I stated before, 404 is a bit problematic status when talking about Restful APIs. Does this status means that the resource was not found? or that there is not mapping to the requested resource? Everyone can decide what to use and where :)

If you’re already using Spring to build your application, and you need to serve a ReST API, Spring MVC can be a good choice to write your REST endpoints.

However, representing errors or problems cleanly in a RESTful way may not be immediately obvious since Spring MVC is so often referenced for building user interfaces. Because there is no direct
UI concept in REST APIs, how then do you use Spring MVC to represent errors or problems in a clean and intuitive way?

This article is Part 1 of a 2 part series. In this article, we’ll cover RESTful error reporting best practice conventions. Part 2 will show how to implement them with Spring MVC in a complete example web application.

RESTful Error Design

If an error occurs, RESTful practices expect that we set an HTTP status code in the response to generally classify why the request failed. It is considered best practice by most REST API designers to re-use the status codes in the HTTP specification whenever possible, since so many existing HTTP clients understand most of these error conditions already and re-use encourages consistent behavior, which is always good for development.

However, there are only 24 commonly referenced HTTP status codes that are used for representing errors; there are 18 4xx codes representing client errors and 6 5xx codes representing server errors (there are other codes defined in specifications like WebDav, but they are not ubiquitous). A problem here is that these 24 are really general purpose codes – they likely don’t describe everything there is to know about a particular problem.

To best help your REST API customers, you ideally want to give them as much information as possible to help them diagnose and hopefully fix the problem. The name of the game is simplicity: the easier it is for them to use your REST API, the more likely it is they will adopt your service.

REST Error Representation

Because the status code is probably not enough, what else then can we provide to assist our end users whenever they encounter errors? An obvious item is human readable error message, to be easily read by developers. But there is even more information that we can add to provide a truly intuitive and helpful error representation.

The folks at Apigee have put together a nice blog article about RESTful error representations (and some great videos too) worth checking out. We want to do something similar.

Here is an example of what we consider a ‘best practice’ REST error response body representation to show relevant error data (the example is JSON, but XML would be fine too depending on what your API serves):

{

    «status»: 404,

    «code»: 40483,

    «message»: «Oops! It looks like that file does not exist.»,

    «developerMessage»: «File resource for path /uploads/foobar.txt does not exist.  Please wait 10 minutes until the upload batch completes before checking again.»,

    «moreInfo»: «http://www.mycompany.com/errors/40483»

}

We’ll describe the properties next.

Status

The status property is merely the same HTTP status code (integer) in the response header. This is a convenience mechanism: by including the status code in the body, any REST client that processes the error has one and only one place to look to fully understand the error: the error representation itself.  There is no need to check header values or other locations to understand the message.

Code

The code property is an error code specific to your particular REST API. It is usually something that conveys information very specific to your problem domain.

This is convenient because of the limitation of having only 24 widely known general purpose HTTP error codes. By using your own codes, you can convey much more specific and richer reasons as to why something failed. Again, the more information that the API client can receive, the better.

In the example above, the code property has a value of 40483. While the general purpose “status”: 404 indicates that the requested resource was not found, perhaps there is an application-specific code of 40483 that indicates not only that the resource wasn’t found, but it wasn’t found due to the fact that it wasn’t yet uploaded to the server.

Granted, this particular ‘uploaded file’ example is somewhat contrived, but the key point here is that your API can convey a much richer set of error information if you leverage your own codes.

TIP:  If your application does not have a specific error code for a particular error, it can be a good idea to default the code value to be the same as the status value. This ensures that the client always sees a code value and does not need to perform ‘does this property exist?’ logic. This is cleaner/easier for API consumers, and that’s a good thing for adoption.

Message

The message property is a nice human readable error message that can potentially be shown directly to an application end user (not a developer).  It should be friendly and easy to understand and convey a concise reason as to why the error occurred.  It should probaby not contain technical information.  Technical information should be in the developerMessage property instead (covered next).

Why is this useful?

If you have a REST API consumer (e.g. your customer), and that consumer wishes to relay the message value directly to the end user, they can do so.  This allows API consumers to write user interfaces and support their own end-users very quickly without much work on their end.  The more things you do to save them time and keep them happy using your API, the better.

DeveloperMessage

The developerMessage property conveys any and all technical information that a developer calling your REST API might find useful.  This is where you might include exception messages, stack traces, or anything else that you think will help a developer.

moreInfo

The moreInfo property specifies a URL that anyone seeing the error message can click (or copy and paste) in a browser.  The target web page should describe the error condition fully, as well as potential solutions to help them resolve the error condition.

This is probably THE most important property of all because the target web page can be freely formatted to represent whatever information you wish.  You could have links to your support department, ‘get help now’ chat dialogs, or whatever else you think might be useful.  Show the developers love, and they’ll continue using your API.

Twilio for example has a great Error and Warning Dictionary that shows this concept in action.  Learn it, love it, replicate it.  Again, as much information as is possible is desired.  Supply whatever you might think is necessary to help your API consumers/customers and keep them happy.

Continue on to part 2, where we show you how to ensure these error messages are easily returned from a Spring MVC-based REST API.

Like what you see? Follow @goStormpath to keep up with the latest releases.

Понравилась статья? Поделить с друзьями:
  • Rest error 401
  • Rest error 400
  • Rest api выдал ошибку wordpress
  • Rest api error codes
  • Rest api error 500