tornado定制json格式错误信息以及源码解析

send_error()和write_error()

用过tornado的Pythoner都知道在tornado.RequestHandler.write(chunk)方法中,如果chunk为是一个dict类型,返回给前端的response将会是json格式。那么如果我们想将错误的提示信息也以json的格式返回给前端需要怎么操作呢?别急,下面我将告诉大家我的方法。

首先讲一下send_error()write_error()两个方法。

send_error(status_code=500, **kwargs)

抛出HTTP错误状态码status_code,默认为500。使用send_error主动抛出错误后tornado会调用write_error()方法进行处理。kwargs将会传递给write_error()方法

我们先来看一下源码:

def send_error(self, status_code=500, **kwargs):
    """Sends the given HTTP error code to the browser.

    If `flush()` has already been called, it is not possible to send
    an error, so this method will simply terminate the response.
    If output has been written but not yet flushed, it will be discarded
    and replaced with the error page.

    Override `write_error()` to customize the error page that is returned.
    Additional keyword arguments are passed through to `write_error`.
    """
    if self._headers_written:
        gen_log.error("Cannot send error response after headers written")
        if not self._finished:
            # If we get an error between writing headers and finishing,
            # we are unlikely to be able to finish due to a
            # Content-Length mismatch. Try anyway to release the
            # socket.
            try:
                self.finish()
            except Exception:
                gen_log.error("Failed to flush partial response",
                              exc_info=True)
        return
    self.clear()

    reason = kwargs.get('reason')
    if 'exc_info' in kwargs:
        exception = kwargs['exc_info'][1]
        if isinstance(exception, HTTPError) and exception.reason:
            reason = exception.reason
    self.set_status(status_code, reason=reason)
    try:
        self.write_error(status_code, **kwargs)
    except Exception:
        app_log.error("Uncaught exception in write_error", exc_info=True)
    if not self._finished:
        self.finish()

大概翻译一下doc说明:

发送错误状态码到浏览器
如果flush()方法已经被调用将不会发送error信息,而是直接终止请求。如果output已经写入缓冲区而没有被flushed,将会被丢弃并替换成error信息
重写write_error()来定制错误页面。kwargs参数将会透传给write_error方法

根据文档和源码我们可以发现,send_error方法会调用set_status(status_code, reason=reason)设置错误码,调用write_error(status_code, **kwargs)生成错误信息,所以下面我们重点看一下生成错误信息的write_error()方法

write_error(status_code, **kwargs)

可以重写此方法来定制自己的错误显示页面。

首先当然还是看一下源码

def write_error(self, status_code, **kwargs):
    """Override to implement custom error pages.

    `write_error` may call `write`, `render`, `set_header`, etc
    to produce output as usual.

    If this error was caused by an uncaught exception (including
    HTTPError), an `exc_info` triple will be available as
    `kwargs["exc_info"]`.  Note that this exception may not be
    the "current" exception for purposes of methods like
    `sys.exc_info()` or `traceback.format_exc`.
    """
    if self.settings.get("serve_traceback") and "exc_info" in kwargs:
        # in debug mode, try to send a traceback
        self.set_header('Content-Type', 'text/plain')
        for line in traceback.format_exception(*kwargs["exc_info"]):
            self.write(line)
        self.finish()
    else:
        self.finish("<html><title>%(code)d: %(message)s</title>"
                    "<body>%(code)d: %(message)s</body></html>" % {
                        "code": status_code,
                        "message": self._reason,
                    })

具体方法的实现就不细看了,反正我们要重写此方法。
方法重写可以在一个基类BaseHandler中定义,此基类继承自tornado.web.RequestHandler
send_error()会调用write_error()并透传status_codekwarg参数,所以我们可以在write_error()方法中获取kwarg参数,构建一个dict并通过调用write(dict)方法返回一个json给前端。

BaseHandler

BaseHandler可以这样写:

## bases.py
class BaseHandler(tornado.web.RequestHandler):
    ...
    def write_error(self, status_code, **kwargs):
        reason = kwargs.get('reason')
        self.write({'status_code': status_code, 'reason': reason})

现在我们在某个Handlerget方法里调用send_error

## handlers.py
from bases import BaseHandler

class TestHandler(BaseHandler):
    def get(self):
        ...
        self.send_error(400, reason='missing args')

用浏览器或者Posetman访问得道结果
状态码为为: Status Code: 400 missing args
response body为:

{
    "status_code": 400,
    "reason": "missing args"
}

哎呦,看起来似乎正是我们想要的
但是有时候我们之所以调用send_error是因为发生了错误,请求不能再继续进行了,但是经测试send_error并不会中断请求,后续的代码依然会被解释器执行。如果后续代码涉及一些敏感操作,我们就不能使用send_error,而是raise一个tornado.web.HTTPError中断请求,并且后续的代码并不会被解释器执行,HTTPError也会调用write_error来生成错误信息。

HTTPError

我们来看一下HTTPError的源码:

class HTTPError(Exception):
    """An exception that will turn into an HTTP error response.

    Raising an `HTTPError` is a convenient alternative to calling
    `RequestHandler.send_error` since it automatically ends the
    current function.

    To customize the response sent with an `HTTPError`, override
    `RequestHandler.write_error`.

    :arg int status_code: HTTP status code.  Must be listed in
        `httplib.responses <http.client.responses>` unless the ``reason``
        keyword argument is given.
    :arg str log_message: Message to be written to the log for this error
        (will not be shown to the user unless the `Application` is in debug
        mode).  May contain ``%s``-style placeholders, which will be filled
        in with remaining positional parameters.
    :arg str reason: Keyword-only argument.  The HTTP "reason" phrase
        to pass in the status line along with ``status_code``.  Normally
        determined automatically from ``status_code``, but can be used
        to use a non-standard numeric code.
    """
    def __init__(self, status_code=500, log_message=None, *args, **kwargs):
        self.status_code = status_code
        self.log_message = log_message
        self.args = args
        self.reason = kwargs.get('reason', None)
        if log_message and not args:
            self.log_message = log_message.replace('%', '%%')

    def __str__(self):
        message = "HTTP %d: %s" % (
            self.status_code,
            self.reason or httputil.responses.get(self.status_code, 'Unknown'))
        if self.log_message:
            return message + " (" + (self.log_message % self.args) + ")"
        else:
            return message

查看源码可以发现,我们可以给HTTPError的构建函数可以传入参数status_codelog_message以及Keyword参数reason等等。log_message将会在stderr中输出到日志。
下面我们将调用send_error改为raise HTTPError试一下:

## handlers.py
from bases import BaseHandler
from tornado.web import HTTPError
...
class TestHandler(BaseHandler):
    def get(self):
        ...
        raise HTTPError(400, reason='missing args')
        print('===ok===')

用浏览器或者Posetman访问得道结果
状态码为为: Status Code: missing args
response body为:

{
    "status_code": 400,
    "reason": null
}

终端输出:

[W 181127 15:19:24 web:2162] 400 GET /test (192.168.56.1) 0.79ms

中可以看到print('===ok===')并没有被执行,但是返回的json并不是我们想要的,write_error并没有在kwargs中获取到reason,我们加个打印看一下kwargs中到底是什么:

## bases.py
class BaseHandler(tornado.web.RequestHandler):
    ...
    def write_error(self, status_code, **kwargs):
        print(kwargs)
        reason = kwargs.get('reason')
        self.write({'status_code': status_code, 'reason': reason})

终端输出:

{'exc_info': (<class 'tornado.web.HTTPError'>, HTTPError(), <traceback object at 0x7ff96410c808>)}
[W 181127 15:28:32 web:2162] 400 GET /test (192.168.56.1) 0.91ms

可以看到kwargs中只有一个参数exc_info对应的是一个元组,第二个元素正是一个HTTPError实例
所以我们可以从这个实例中获取传入的reason

## bases.py
class BaseHandler(tornado.web.RequestHandler):
    ...
    def write_error(self, status_code, **kwargs):
        reason = kwargs.get('reason')

        if 'exc_info' in kwargs:
            exception = kwargs['exc_info'][1]
            if isinstance(exception, web.HTTPError) and exception.reason:
                reason = exception.reason
        self.write({'status_code': status_code, 'reason': reason})

再来试一下:
状态码为为: Status Code: missing args
response body为:

{
    "status_code": 400,
    "reason": "missing args"
}

不错不错!
但是终端并没有输出错误的提示信息,不利于我们通过日志进行错误排查,我这样试试:

## bases.py
class BaseHandler(tornado.web.RequestHandler):
    ...
    def write_error(self, status_code, **kwargs):

        # 获取send_error中的reason
        reason = kwargs.get('reason', 'unknown')

        # 获取HTTPError中的log_message作为reason
        if 'exc_info' in kwargs:
            exception = kwargs['exc_info'][1]
            if isinstance(exception, web.HTTPError) and exception.log_message:
                reason = exception.log_message
        self.write({'status_code': status_code, 'reason': reason})
## handlers.py
from bases import BaseHandler
from tornado.web import HTTPError
...
class TestHandler(BaseHandler):
    def get(self):
        ...
        # reason换成log_message
        raise HTTPError(400, log_message='missing args')

用浏览器或者Posetman访问得道结果
状态码为为: Status Code: 400 Bad Request
因为我们没有在HTTPError的构建函数中传reasontornado使用了默认的Bad Request作为reason
response body为:

{
    "status_code": 400,
    "reason": "missing args"
}

终端输出:

[W 181127 15:49:37 web:1667] 400 GET /test (192.168.56.1): missing args
[W 181127 15:49:37 web:2162] 400 GET /test (192.168.56.1) 0.85ms

多了一条错误信息,输出的正是HTTPError.log_message状态码也是标准的400 Bad Request nice!

为什么用log_message作为reason呢?因为我们调用RequestHandler.get_argument(name)获取前端传递的参数时,如果未获取到相应的参数name会主动raise MissingArgumentError(name)MissingArgumentErrorHTTPError的子类,其构建函数中没有reason参数,name传递给了log_message(当然你也可自己定义错误类)。

MissingArgumentError源码:

class MissingArgumentError(HTTPError):
    """Exception raised by `RequestHandler.get_argument`.

    This is a subclass of `HTTPError`, so if it is uncaught a 400 response
    code will be used instead of 500 (and a stack trace will not be logged).

    .. versionadded:: 3.1
    """
    def __init__(self, arg_name):
        super(MissingArgumentError, self).__init__(
            400, 'Missing argument %s' % arg_name)
        self.arg_name = arg_name

至此,终于有了优雅的实现方式,现在可以放心的去植发啦-_-!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容