Errors are an essential part of a REST API system. Error instances must follow a particular structure so the client developer can correctly handle them at the client side. We had set a proper error structure at the beginning of creating REST APIs. It’s as follows:
{
"error": {
"message": "Error description",
"code": 400
}
}
Any error occurring during client server communication would follow the above format. Code is the returned status code and message is a brief description of the error. To raise an error we used an _error_abort()
function which was an abstraction over Flask-RESTplus abort()
. We defined an error structure inside _error_abort()
and passed it to abort()
.
def _error_abort(code, message):
error = {
'code': code,
'message': message,
}
abort(code, error=error)
This method had its limitations. Since the error handlers were not being overidden, only errors raised through _error_abort()
had the defined structure. So if an Internal Server error occurred, the returned error response wouldn’t follow the format.
To overcome this, we wrote our own exceptions for errors and created error handlers to handle them. We first made the response structure more detailed, so the client developer can understand what kind of error is being returned.
{
"error": {
"code": 400,
"message": "'name' is a required parameter",
"status": "INVALID_FIELD",
"field": "name"
}
}
The above is an example of a validation error.
The “status” key helps making the error more unique. For example we use 400 status code for both validation errors and invalid service errors (“Service not belonging to said event”). But both have different statuses: “INVALID_FIELD” and “INVALID_SERVICE”.
The “field” key is only useful in the case validation errors, where it names the field that did not pass the validation. For other errors it remains null
.
I first documented what kind of errors we would need.
Code |
Status |
Field |
Description |
401 |
NOT_AUTHORIZED |
null |
Invalid token or user not authenticated |
400 |
INVALID_FIELD |
“field_name” |
Missing or invalid field |
400 |
INVALID_SERVICE |
null |
Service ID mentioned in path does not belong to said Event |
404 |
NOT_FOUND |
null |
Event or Service not found |
403 |
PERMISSION_DENIED |
null |
User role not allowed to perform such action |
500 |
SERVER_ERROR |
null |
Internal server error |
Next part was creating exception classes for each one these. I created a base error class that extended the python Exception
class.
class BaseError(Exception):
"""Base Error Class"""
def __init__(self, code=400, message='', status='', field=None):
Exception.__init__(self)
self.code = code
self.message = message
self.status = status
self.field = field
def to_dict(self):
return {'code': self.code,
'message': self.message,
'status': self.status,
'field': self.field, }
The to_dict()
method would help when returning the response in error handlers.
I then extended this base class to other error classes. Here are three of them:
class NotFoundError(BaseError):
def __init__(self, message='Not found'):
BaseError.__init__(self)
self.code = 404
self.message = message
self.status = 'NOT_FOUND'
class NotAuthorizedError(BaseError):
def __init__(self, message='Unauthorized'):
BaseError.__init__(self)
self.code = 401
self.message = message
self.status = 'NOT_AUTHORIZED'
class ValidationError(BaseError):
def __init__(self, field, message='Invalid field'):
BaseError.__init__(self)
self.code = 400
self.message = message
self.status = 'INVALID_FIELD'
self.field = field
I then defined the error handlers for the api:
@api.errorhandler(NotFoundError)
@api.errorhandler(NotAuthorizedError)
@api.errorhandler(ValidationError)
@api.errorhandler(InvalidServiceError)
def handle_error(error):
return error.to_dict(), getattr(error, 'code')
For overriding the default error handler, Flask-RESTplus let’s you create one with the same decorator, but without passing and argument to it.
@api.errorhandler
def default_error_handler(error):
"""Returns Internal server error"""
error = ServerError()
return error.to_dict(), getattr(error, 'code', 500)
I had set the default error to be the internal server error.
class ServerError(BaseError):
def __init__(self, message='Internal server error'):
BaseError.__init__(self)
self.code = 500
self.message = message
self.status = 'SERVER_ERROR'
Now raising any of these error classes would activate the error handlers and a proper response would be sent to the client.
You must be logged in to post a comment.