Coffee. On the rocks!

Error handling and logging in flask-restful

Thu 14 November 2013

In my previous post i talked about how to do some request and response processing that you can do with flask-restful. In this post i will handle some errors in the application and log them using flask-restful. Here is the example from the previous post

from flask import Flask
from flask.ext.restful import reqparse, Resource, Api

class Data(Resource):
    def post(self):
        parser = reqparse.RequestParser()
        parser.add_argument('app_key', type=str, required=True)
        parser.add_argument('device_id', type=int, required=True)
        parser.add_argument('auth_token', type=str, required=True)
        parser.add_argument('tracking_data', type=str, required=True)
        request_params = parser.parse_args()
        result = process_the_request(request_params)
        response = {
            'result': result,
            'error_code': 0
        }
        return response

app = Flask(__name__)
api = Api(app)

api.add_resource(Data, '/data')

if __name__ == '__main__':
    app.run(port=8000)

Now lets say the function process_the_request throws some exception. Now you can catch it there, log the exception and return an appropriate response. But imagine if you have 10 such resources. Why not have all the exception handling and logging in one place? The way to go here is subclass the Api class and override its handle_error method. Lets say the you want to handle errors which would otherwise generate an HTTP 500 error and return some message.

from flask.ext.restful import Api

class MyApi(Api):
    def handle_error(self, e):
        code = getattr(e, 'code', 500)
        if code == 500:      # for HTTP 500 errors return my custom response
            do_some_error_handling_here_for_excpetion(e)
            return self.make_response({'message': "something went wrong, i'll fix it soon", 'error_code': 'no idea'}, 500)
        return super(MyApi, self).handle_error(e) # for all other errors than 500 use flask-restful's default error handling

Now whenever such error occurs you want to log the error and better log the traceback also. This can be done as

import traceback
from flask import Flask, request
from flask.ext.restful import reqparse, Resource, Api
import async_tasks         # Some asynchronous tasks that i am performing using celery

class MyApi(Api):
    def handle_error(self, e):
        code = getattr(e, 'code', 500)
        if code == 500:
            response = {
                'status_code': 500,
                'data': traceback.format_exc()    # this is the traceback for the current exception
            }
            request_params = extract_some_request_params(request)
            async_tasks.log_request.delay(request_params, response)     # a celery task
            return self.make_response({'messasge': 'something went wrong'}, 500)
        return super(MyApi, self).handle_error(e)     # For non 500 errors

In the above code i check the error and if its 500 and i create i response dictionary that has 2 keys error_code and data. Here data contains the traceback of the exception that caused the error. Then i log this error to my database asynchronously.

What if i want to log all my requests and responses no matter if they generated an exception or not. For this i will use a Flask signal request_finished that Flask sends after a request is processed and response is generated(you need to install the blinker library to use signals so just pip install blinker). Now i subscribe to the signal request_finished by calling its connect method

request_finished.connect(log_response, app)

Here is subscribe to request_finished and whenever i get this signal (and when it is sent by app) i call the log_response function. The log_response function get the response Flask generated for the current request.

from flask import Flask, request, request_finished
app = Flask(__name__)

def log_response(sender, response, **extra):
    if response.status_code != 500:
        request_params = extract_some_request_params(request)
        response_params = extract_some_response_params(response)
        async_tasks.log_request(request_params, response_params)

request_finished.connect(log_response, app)

Now the API code should look some thing like this

import traceback
from flask import Flask, request, request_finished
from flask.ext.restful import reqparse, Resource, Api
import async_tasks
app = Flask(__name__)

def log_response(sender, response, **extra):
    if response.status_code != 500:          # because 500 errors are already being logged by handle_error
        request_params = extract_some_request_params(request)
        response_params = extract_some_response_params(response)
        async_tasks.log_request(request_params, response_params)

request_finished.connect(log_response, app)

class MyApi(Api):
    def handle_error(self, e):
        code = getattr(e, 'code', 500)
        if code == 500:
            response = {
                'status_code': 500,
                'data': traceback.format_exc()          # this is the traceback for the current exception
            }
            request_params = extract_some_request_params(request)
            async_tasks.log_request.delay(request_params, response) # a celery task
            return self.make_response({'messasge': 'something went wrong'}, 500)
        return super(MyApi, self).handle_error(e)

Its helpful if you give some error_codes in your API response for some common exceptions like UserNotRegistered or AppVersionNotSupported. Say you have a function that takes an exception and returns a response with an error code and an appropriate message. Call it exc_message.

exc_message(UserNotRegistered) returns {'error_code': 1, 'message': 'Register'}
exc_message(AppVersionNotSupported) returns {'error_code': 2, 'message': 'Update'}

First define your exceptions

class UserNotRegistered(Exception):
    def __init__(self):
        self.message = 'Register'
        self.error_code = 1

class AppVersionNotSupported(Exception):
    def __init__(self):
        self.message = 'Update'
        self.error_code = 2

Now define handle_error like

def handle_error(self, e):
    code = getattr(e, 'code', 500)
    if code == 500:
        response = {
        'status_code': 500,
        'data': traceback.format_exc()
        }
        request_params = extract_some_request_params(request)
        async_tasks.log_request.delay(request_params, response)     # a celery task
        result = exc_message(e)         # generate appropriate message for the excpetion
        return self.make_response(result, 500)
    return super(MyApi, self).handle_error(e)

This entry was tagged as flask flask-restful error handling logging

blog comments powered by Disqus