Лучший способ обработки ошибок в обработчике запросов торнадо

Есть два похожих обработчика: AgeHandler1 и AgeHandler2. В первом мы просто вызываем конкретное исключение, чтобы вернуть сообщение об ошибке, во втором — вручную возвращаем сообщение об ошибке. Что Вы думаете об этих двух методах? Какой метод предпочтительнее для большого проекта? Любые другие лучшие практики?

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()

Ответы:

"""
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>
"""

person nanvel    schedule 14.10.2014    source источник


Ответы (3)


В общем, лучший подход — переопределить RequestHandler.write_error. Это похоже на ваш первый подход, но вам не нужен try/except в теле обработчика, потому что Tornado сделает это за вас.

Явные тесты, такие как во втором примере, также хороши, но нецелесообразно перехватывать все возможные ошибки таким образом, поэтому вам всегда нужно что-то для обработки неперехваченных исключений.

person Ben Darnell    schedule 15.10.2014

Перезапись write_error работает очень хорошо. Что я делаю в своих проектах, так это пытаюсь поймать любые коды состояния 500. Затем я отправляю их себе через слабину (у меня достаточно низкий трафик, поэтому частота очень низкая).

Вот код для извлечения чистой трассировки стека из write_error. Обратите внимание, что в этом примере я также выдергиваю все ссылки на gen.py, concurrent.py или web.py, что значительно упрощает трассировку стека.

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")  

Вывод выглядит следующим образом:

  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.
person Mike N    schedule 21.03.2018

Для больших проектов я бы попытался абстрагироваться от номеров ошибок, особенно потому, что определение кодов состояния HTTP не входит в ваши обязанности. Насколько я помню, есть по крайней мере одна пара кодов состояния с проблемной семантикой. Я не помню, где они.

Но для более крупного проекта я бы порекомендовал вам определить свои собственные категории ошибок, которые вы хотите поддерживать, и централизованно сопоставить эти категории с HTTP-кодами по мере необходимости. Когда позже вы обнаружите, что вам следует использовать другой код состояния для какой-то категории ошибок, вы можете сделать это централизованно.

Логически я бы попытался извлечь как можно больше знаний из конкретной процедуры обработки. Модель исключения, конечно, здесь удобна, но аналогичного можно достичь с помощью вызова функции для обработки ошибок, например:

...
if age < 1 or age > 200:
   return self.errorResult('Wrong age value.', WRONG_VALUE)
...

or

...
if age < 1 or age > 200:
   return self.wrongValue('Wrong age value.')
...
person Juergen    schedule 15.10.2014