Error handling in Restful API

brief introduction

With the rise of mobile development and front-end development, more and more Web back-end applications tend to implement Restful APIs.The Restful API is a simple, easy-to-use front-end and back-end separation scheme that simply handles client requests and returns the results without regard to page rendering, alleviating some of the burden on back-end developers.However, it is precisely because the Restful API does not need to consider page rendering that prevents it from displaying error information on the page.That means that when an error occurs, it can only tell users and developers the appropriate error message by returning an error response and prompt them what to do next.This article discusses error handling in the Restful API.

Design Error Information

When the Restful API needs to throw an error, we need to consider what information the error should contain.Let's first look at the error messages from Github, Google, Facebook, Twitter, Twilio.

Github (use http status)

{
  "message": "Validation Failed",
  "errors": [
    {
      "resource": "Issue",
      "field": "title",
      "code": "missing_field"
    }
  ]
}

Google (use http status)

{
  "error": {
    "errors": [
      {
        "domain": "global",
        "reason": "insufficientFilePermissions",
        "message": "The user does not have sufficient permissions for file {fileId}."
      }
    ],
    "code": 403,
    "message": "The user does not have sufficient permissions for file {fileId}."
  }
}

Facebook (use http status)

{
  "error": {
    "message": "Message describing the error", 
    "type": "OAuthException",
    "code": 190,
    "error_subcode": 460,
    "error_user_title": "A title",
    "error_user_msg": "A message",
    "fbtrace_id": "EJplcsCHuLu"
  }
}

Twitter (use http status)

{
  "errors": [
    {
      "message": "Sorry, that page does not exist",
      "code": 34
    }
  ]
}

Twilio (use http status)

{
  "code": 21211,
  "message": "The 'To' number 5551234567 is not a valid phone number.",
  "more_info": "https://www.twilio.com/docs/errors/21211",
  "status": 400
}

Looking at these structures, you can see that they have something in common:

  • Both utilize Http status codes
  • Some returned business error codes
  • All provide error messages for users to see
  • Some provide error messages for developers

Http Status Code

Http status codes are a good way to represent error types in the Restful API because they define many abstract error types.Although Http status codes define a large number of error types, there are not many commonly used status codes in practical applications, usually in the following areas:

  • API is working (200,201)
  • Client error (400, 401, 403, 404)
  • Server side error (500,503)

Business Error Code

Many times, we customize error codes based on business type.These business error codes do not overlap with the Http status codes, so we can return the business error codes to prompt the user/developer for the type of error.

Error message for user

When an error occurs, we need to prompt the user how to handle it, which is usually necessary.You can see that in each of the above examples, error messages are returned to the user.

Error messages for developers

If our API needs to be open to third-party developers, then we need to consider returning some error messages to the developers.

Design Error Type

As we mentioned earlier, Http status codes can be used to classify error types.Typically, what we call categorization is to categorize client errors, which are errors of type 4xx.

Of these types of errors, the most common are:

  • 400 Bad Request
    The current request could not be understood by the server due to a syntax error.Clients should not submit this request repeatedly unless modified.
    This status code is usually returned when the request parameter is not valid or is malformed.
  • 401 Unauthorized
    The current request requires user authentication.
    This status code is usually used when accessing some protected API s without a login.
  • 403 Forbidden
    The server understood the request but refused to execute it.Unlike the 401 response, authentication does not help.
    This status code is often used when there is no privilege to operate on a resource, such as modifying/deleting a resource that does not belong to that user.
  • 404 Not Found
    The request failed and the desired resource for the request was not found on the server.
    This status code is usually returned when a resource is not found.

Although we can use Http status codes to indicate the type of error, in practical applications, if only Http status codes are used, our code is populated with Http status codes:

// Node.js
if (!res.body.title) {
  res.statusCode = 400
}

if (!user) {
  res.statusCode = 401
}

if (!post) {
  res.statusCode = 404
}

The above implementation is also acceptable in small projects and becomes cumbersome to maintain when the project becomes larger and more demanding.To improve the readability and maintainability of errors, we need to categorize the various errors.My personal habit is to classify errors into the following types:

  • Format error (FORMAT_INVALID)
  • Data does not exist (DATANOTFOUND)
  • Data already exists (DATA_EXISTED)
  • Invalid data (DATA_INVALID)
  • Logon error (LOGIN_REQUIRED)
  • Insufficient permissions (PERMISSION_DENIED)

After misclassification, we throw errors more intuitively:

if (!res.body.title) {
  throw new Error(ERROR.FORMAT_INVALID)
}

if (!user) {
  throw new Error(ERROR.LOGIN_REQUIRED)
}

if (!post) {
  throw new Error(ERROR.DATA_NOT_FOUND)
}

if (post.creator.id !== user.id) {
  throw new Error(ERROR.PERMISSION_DENIED)
}

This form is much more convenient and easier to maintain than the above dead-state code.The problem with this is that the specified error information cannot be returned based on the error type.

Custom error type

To return the specified error information based on the error type, we can customize the error.Suppose we customize the structure of the error as follows:

{
  "type": "",
  "code": 0,
  "message": "",
  "detail": ""
}

We need to do the following:

  • Automatically set type, code, message based on error type
  • detail is optional and describes the specific cause of the error
const ERROR = {
  FORMAT_INVALID: 'FORMAT_INVALID',
  DATA_NOT_FOUND: 'DATA_NOT_FOUND',
  DATA_EXISTED: 'DATA_EXISTED',
  DATA_INVALID: 'DATA_INVALID',
  LOGIN_REQUIRED: 'LOGIN_REQUIRED',
  PERMISSION_DENIED: 'PERMISSION_DENIED'
}

const ERROR_MAP = {
  FORMAT_INVALID: {
    code: 1,
    message: 'The request format is invalid'
  },
  DATA_NOT_FOUND: {
    code: 2,
    message: 'The data is not found in database'
  },
  DATA_EXISTED: {
    code: 3,
    message: 'The data has exist in database'
  },
  DATA_INVALID: {
    code: 4,
    message: 'The data is invalid'
  },
  LOGIN_REQUIRED: {
    code 5,
    message: 'Please login first'
  },
  PERMISSION_DENIED: {
    code: 6,
    message: 'You have no permission to operate'
  }
}

class CError extends Error {
  constructor(type, detail) {
    super()
    Error.captureStackTrace(this, this.constructor)

    let error = ERROR_MAP[type]
    if (!error) {
      error = {
        code: 999,
        message: 'Unknow error type'
      }
    }

    this.name = 'CError'
    this.type = error.code !== 999 ? type : 'UNDEFINED'
    this.code = error.code
    this.message = error.message
    this.detail = detail
  }
}

Once you've customized the error, it's easier to call it:

// in controller
if (!user) {
  throw new CError(ERROR.LOGIN_REQUIRED, 'You should login first')
}

if (!req.body.title) {
  throw new CError(ERROR.FORMAT_INVALID, 'Title is required')
}

if (!post) {
  throw new CError(ERROR.DATA_NOT_FOUND, 'The post you required is not found')
}

Finally, there is one remaining problem, setting the status code based on the type of error, and returning the error information to the client.

Capture error information

After a custom error is thrown in the Controller, we need to catch it before we can return it to the client.Assuming that we use koa 2 as a web framework to develop restful api, all we need to do is add middleware for error handling:

module.exports = async function errorHandler (ctx, next) {
  try {
    await next()
  } catch (err) {

    let status

    switch (err.type) {
      case ERROR.FORMAT_INVALID:
      case ERROR.DATA_EXISTED:
      case ERROR.DATA_INVALID:
        status = 400
        break
      case ERROR.LOGIN_REQUIRED:
        status = 401
      case ERROR.PERMISSION_DENIED:
        status = 403
      case ERROR.DATA_NOT_FOUND:
        status = 404
        break
      default:
        status = 500
    }

    ctx.status = status
    ctx.body = err
  }
}

// in app.js
app.use(errorHandler)
app.use(router.routes())

In this way, we can gracefully handle error messages in the Restful API.

Reference material

https://zh.wikipedia.org/zh-hans/HTTP status codehttps://www.loggly.com/blog/node-js-error-handling/http://blog.restcase.com/rest-api-error-codes-101/https://apigee.com/about/blg/technology/restful-api-design-what-about-errorshttp://stackoverflow.com/questions/942951/rest-api-error-return-good-practiceshttp://goldbergyoni.com/checklist-best-practices-of-node-js-error-handling/http://blogs.mulesoft.com/dev/api-dev/api-best-practices-response-handling/https://developers.facebook.com/docs/graph-api/using-graph-api/#errorshttps://developers.google.com/drive/v3/web/handle-errorshttps://developer.github.com/v3/#client-errorshttps://dev.twitter.com/overview/api/response-codeshttps://www.twilio.com/docs/api/errors

Keywords: Google github Database REST

Added by Guldstrand on Thu, 01 Aug 2019 04:32:04 +0300