Error Handling

Note

Take a look at Sanic’s Exceptions documentation to better understand how Insanic’s error handling works.

Insanic’s error handling is done with Sanic’s error handling functionality, but with Insanic’s own exception and error definitions. Before we move onto the components that comprise of an Insanic exception, let’s take a look at a quick example.

# in example/app.py
from insanic import Insanic
from insanic.conf import settings
from insanic.errors import GlobalErrorCodes
from insanic.exceptions import APIException

__version__ = '0.1.0'

settings.configure()
app = Insanic('example', version=__version__)

@app.route('/help')
def help_view(request, *args, **kwargs):
    raise APIException("Help me! Something blew up!",
                       error_code=GlobalErrorCodes.error_unspecified,
                       status_code=400)

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8000)

With this piece of code, let’s try running it…

$ python app.py

Now by sending a request to the server…

curl -i http://0.0.0.0:8000/help
HTTP/1.1 400 Bad Request
Content-Length: 139
Content-Type: application/json
Connection: keep-alive
Keep-Alive: 60

{
    "message":"An unknown error occurred",
    "description":"Help me! Something blew up!",
    "error_code":{
        "name":"insanic_error_unspecified",
        "value":999998
    }
} # response was formatted for readability

From the response there are a couple components we need to cover to understand how Insanic’s error handler works.

  1. The GlobalErrorCodes

  2. The APIException

  3. The response.

1. Error Codes

In a distributed system, errors can happen anywhere. It can happen within the service you have created, it could happen down the road where you made a request to another service for some additional information, or even worse, the other service could get a different error message from a request that it had to make to aggregate the response.

As a result, the only way to keep track and possibly debug the situation, specific pin point traceability was very important. Of course, just returning a 400 Bad Request error response might suffice, but in some instances, an application may have to react in a certain manner if it receives a particular 400 Bad Request error. For example, rolling back a database commit only for a specific error.

Insanic provides common error codes, accessible in insanic.errors.GlobalErrorCodes but each service may provide their own specific error codes with one restriction. The Error Code must be an Enum type.

To create your own:

# example/errors.py
from enum import Enum

class MyErrorCodes(Enum):
    not_going_fast_enough = 10001
    too_slow = 10002
    help_me = 10003

When set to the error_code attribute in the Insanic’s exception (we will get to this a bit later), the enum will be unpacked by Insanic’s Error Handler to a JSON object. So in our example, MyErrorCodes.not_going_fast_enough will be unpacked like so:

{
    "name":"not_going_fast_enough",
    "value":10001
}

2. Insanic APIException

To actually create the error, Insanic provides its own APIException base class for its own error handling. This exception will create the response as shown in the first example.

There are 4 attributes to the exception.

  1. status_code: an integer representing the status code of the response.

  2. description: a string with human readable description of the error.

  3. error_code: an Enum as explained in the ErrorCode section above.

  4. message: a string with a general message.

There are several exceptions provided as base templates, but it is up to the developer to define how detailed the exceptions will be.

Let’s create some example execeptions:

# example/exceptions.py
from insanic import status
from insanic.exceptions import APIException, BadRequest

from .errors import MyErrorCodes

class TooSlowException(APIException):
    status_code = status.HTTP_408_REQUEST_TIMEOUT
    description = "Too slow!"
    error_code = MyErrorCodes.too_slow

class MyBadRequest(BadRequest):
    error_code = MyErrorCodes.not_going_fast_enough

And now to use these exceptions…

# example/views.py
from insanic import status
from insanic.exceptions import APIException
from .app import app  # your insanic application
from .errors import MyErrorCodes
from .exceptions import TooSlowException

@app.route('/too_slow`)
def too_slow_view(request, *args, **kwargs):
    raise TooSlowException()

@app.route('/very_slow')
def very_slow_view(request, *args, **kwargs):
    raise TooSlowException("This is very slow!")

@app.route('/help_me_too_slow')
def help_me_too_slow(request, *args, **kwargs):
    raise APIException(
        "HELP ME!",
        error_code=MyErrorCodes.help_me,
        status_code=status.HTTP_504_GATEWAY_TIMEOUT
    )

3. Putting ErrorCodes and Exceptions together

With exceptions and error codes defined, Insanic’s error handler will serialize the exception to the error response structure as shown in the example.

class TooSlowException(APIException):
    status_code = status.HTTP_408_REQUEST_TIMEOUT
    description = "Too slow!"
    error_code = MyErrorCodes.too_slow

With this exception we created above, Insanic’ Error Handler will create this response.

{
    "message":"An unknown error occurred",
    "description":"Too slow!",
    "error_code":{
        "name":"too_slow",
        "value":10002
    }
}
  • The status_code is the status code of the response.

  • The description is the description.

  • The message is the message attribute in APIException.

  • The error_code is the unpacked enum.

What about NON-Insanic Exceptions?

Any Sanic Exceptions will automatically be converted to an Insanic Exception and will try and serialize the message into Insanic’s error message format.

from sanic.exceptions import ServiceUnavailable

@app.route('/sanic')
def raise_sanic(request, *args, **kwargs):
    raise ServiceUnavailable('sanic error')

Will result in…

$ curl -i http://0.0.0.0:8000/sanic
HTTP/1.1 503 Service Unavailable
Content-Length: 126
Content-Type: application/json
Connection: keep-alive
Keep-Alive: 60

{
    "message":"Service Unavailable",
    "description":"sanic error",
    "error_code":{
        "name":"insanic_error_unspecified",
        "value":999998
    }
}

Any NON-Insanic and NON-Sanic exceptions raised during the process of a request will default to a 500 Internal Server Error.

@app.route('/builtin')
def raise_sanic(request, *args, **kwargs):
    raise SystemError('sanic error')
$ curl -i http://localhost:8000/builtin
HTTP/1.1 500 Internal Server Error
Content-Length: 167
Content-Type: application/json
Connection: keep-alive
Keep-Alive: 60

{
    "message":"Server Error",
    "description":"Something has blown up really bad. Somebody should be notified?",
    "error_code":{
        "name":"insanic_unknown_error",
        "value":999999
    }
}

See Also…