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_code
和kwarg
参数,所以我们可以在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})
现在我们在某个Handler
的get
方法里调用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_code
、log_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的构建函数中传reason
,tornado
使用了默认的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)
。MissingArgumentError
是HTTPError
的子类,其构建函数中没有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
至此,终于有了优雅的实现方式,现在可以放心的去植发啦-_-!