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("%(code)d: %(message)s"
                    "%(code)d: %(message)s" % {
                        "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 ` 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': (, HTTPError(), )}
[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

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

你可能感兴趣的:(tornado定制json格式错误信息以及源码解析)