There are two similar handlers: AgeHandler1 and AgeHandler2. In the first one we simply raise a specific exception to return an error message, in the second - we manually return an error message. What You think about these two methods? Which method is preferable for a large project? Any other best practices?
import logging
import os.path
import traceback
from sys import exc_info
from tornado import web, options, ioloop
logger = logging.getLogger(__name__)
class MyAppException(Exception):
def __init__(self, message, code=400, *args, **kwargs):
self.message = message
self.code = code
return super(MyAppException, self).__init__(*args, **kwargs)
def __str__(self):
return self.message
class MyAppBaseHandler(web.RequestHandler):
def handle_exception(self, e):
exc_type, exc_obj, exc_tb = exc_info()
logger.error(''.join([line for line in traceback.format_exception(
exc_type, exc_obj, exc_tb)]))
if isinstance(exc_obj, MyAppException):
self.set_status(exc_obj.code)
self.write({'error': {
'message': u'{exc_obj}'.format(exc_obj=exc_obj.message)}})
else:
self.set_status(500)
fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
self.write({'error': {
'message': u'{exc_obj} in {fname} at {line}'.format(
exc_obj=exc_obj, fname=fname, line=exc_tb.tb_lineno)}})
class AgeHandler1(MyAppBaseHandler):
def get(self):
try:
age = self.get_argument('age')
age = int(age)
if age < 1 or age > 200:
raise MyAppException('Wrong age value.')
self.write('Your age is {age}'.format(age=age))
except Exception as e:
self.handle_exception(e)
class AgeHandler2(MyAppBaseHandler):
def get(self):
age = self.get_argument('age')
age = int(age)
if age < 1 or age > 200:
self.set_status(400)
self.write('Wrong age value.')
return
self.write('Your age is {age}'.format(age=age))
class MyApplication(web.Application):
def __init__(self, **kwargs):
kwargs['handlers'] = [
web.url(r'/age1', AgeHandler1, name='age1'),
web.url(r'/age2', AgeHandler2, name='age2'),
]
kwargs['debug'] = False
super(MyApplication, self).__init__(**kwargs)
if __name__ == '__main__':
options.parse_command_line()
application = MyApplication()
application.listen(5000)
ioloop.IOLoop.instance().start()
Responses:
"""
http://127.0.0.1:5000/age1
500: {"error": {"message": "HTTP 400: Bad Request (Missing argument age) in app.py at 44"}}
---
http://127.0.0.1:5000/age1?age=10
200: Your age is 10
---
http://127.0.0.1:5000/age1?age=201
400: {"error": {"message": "Wrong age value."}}
---
http://127.0.0.1:5000/age1?age=abc
500: {"error": {"message": "invalid literal for int() with base 10: 'abc' in app.py at 45"}}
http://127.0.0.1:5000/age2
400: <html><title>400: Bad Request</title><body>400: Bad Request</body></html>
---
http://127.0.0.1:5000/age2?age=10
200: Your age is 10
---
http://127.0.0.1:5000/age2?age=201
400: Wrong age value.
---
http://127.0.0.1:5000/age2?age=abc]
500: <html><title>500: Internal Server Error</title><body>500: Internal Server Error</body></html>
"""
In general the best approach is to override RequestHandler.write_error
. This is similar to your first approach, but you don't need the try/except in the body of the handler because Tornado will handle this for you.
Explicit tests like those in your second example are also good, but it's impractical to catch all possible errors this way so you'll always need something to handle uncaught exceptions.
Overwriting write_error works very well. What I do in my projects is I try to catch any 500
status codes. I then send them to myself over slack (my traffic is low enough that the frequency is very low).
Here's the code to extract a clean stack trace from write_error
. Note in this example I also yank out any references to 'gen.py', 'concurrent.py' or 'web.py', which makes for much cleaner stack traces.
import tornado.web, traceback, logging
class MyRequestHandler(tornado.web.RequestHandler):
def write_error(self,status_code,**kwargs):
if status_code == 500:
excp = kwargs['exc_info'][1]
tb = kwargs['exc_info'][2]
stack = traceback.extract_tb(tb)
clean_stack = [i for i in stack if i[0][-6:] != 'gen.py' and i[0][-13:] != 'concurrent.py']
error_msg = '{}\n Exception: {}'.format(''.join(traceback.format_list(clean_stack)),excp)
# do something with this error now... e.g., send it to yourself
# on slack, or log it.
logging.error(error_msg) # do something with your error...
# don't forget to show a user friendly error page!
self.render("oops.html")
The output looks like this:
File "app.py", line 55, in get
assert 1==2,"A fake error to trigger a critical alert."
Exception: A fake error to trigger a critical alert.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With