0%

tornado.web.RequestHandler 异常处理

RequestHandler 异常处理

之前的笔记中已经提到过 RequestHandler_execute 方法完全处于一个异常处理块中,所以异常处理入口 _handle_request_exception 便在这里开始处理流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def _handle_request_exception(self, e):
if isinstance(e, Finish):
# Not an error; just finish the request without logging.
if not self._finished:
self.finish()
return
self.log_exception(*sys.exc_info())
if self._finished:
# Extra errors after the request has been finished should
# be logged, but there is no reason to continue to try and
# send a response.
return
if isinstance(e, HTTPError):
if e.status_code not in httputil.responses and not e.reason:
gen_log.error("Bad HTTP status code: %d", e.status_code)
self.send_error(500, exc_info=sys.exc_info())
else:
self.send_error(e.status_code, exc_info=sys.exc_info())
else:
self.send_error(500, exc_info=sys.exc_info())

由前面的代码可以看到,与请求异常处理相关的自定义异常类型是 FinishHTTPError,方法有 log_exceptionsend_error

_handle_request_exception 的处理流程中,除去 Finish 类型外的所有异常都需要调用 log_exception 方法来记录异常信息。如果响应没有结束,还需要尝试向客户端响应异常信息(只所以说是 “尝试”,是因为我们无法修改已经 flush 到客户端的数据,只能在尚未 flush 过数据时响应异常信息)。

Finish

Finish 是一个特别的自定义异常类型,为应用程序提供一种提前结束请求的方式。因抛出 Finish 而结束的请求不会输出一个异常响应(即不被视为请求处理异常)。如代码中所示,在 RequestHandler 请求处理过程中抛出 Finish 时,如果没有调用 finish 则调用 finish 结束请求,并立即返回,结束处理流程。这样一来后续与异常处理相关的方法就不会被调用,也就不会影响到已经 flush 的内容。

HTTPError

HTTPError 用来包装一个异常,将其转换为一个 HTTP 错误响应。与 Finish 一样,抛出一个 HTTPError 便会自动结束一个请求,相对于直接调用 send_error 来向客户端输出一个 HTTP 错误响应更方便。

_handle_request_exception 的代码可知,对于一般的未捕获的异常,直接响应为 HTTP 500 错误。对于 HTTPError 异常实例,如果其没有设置 HTTP 状态码和错误原因也响应为 HTTP 500 错误,否则按照 HTTPError 实例提供的错误码响应。

log_exception method

log_exception 为请求处理器提供记录未处理异常的日志功能,RequestHandler 的子类可以覆写该方法来自定义日志的等级和输出内容、格式等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def log_exception(self, typ, value, tb):
if isinstance(value, HTTPError):
if value.log_message:
format = "%d %s: " + value.log_message
args = ([value.status_code, self._request_summary()] +
list(value.args))
gen_log.warning(format, *args)
else:
app_log.error("Uncaught exception %s\n%r", self._request_summary(),
self.request, exc_info=(typ, value, tb))

def _request_summary(self):
return self.request.method + " " + self.request.uri + \
" (" + self.request.remote_ip + ")"

默认情况下,HTTPError 通过 tornado.general 日志实例记录为 “warning” 等级,并且不会记录异常堆栈信息。其他异常类型则通过 tornado.application 日志实例记录为 “error” 等级,并有完整的异常堆栈信息。

另外,_request_summary 方法只能输出请求的 HttpMethod、uri、remote_ip 信息。发生非 HTTPError 类型异常时,甚至连上述信息都没有记录。这在实际使用时可能并不够用,通常我们可能还需要将请求(GET/POST)参数一并记录下来分析。

send_error method

send_error 在请求处理器捕获到未处理异常时尝试向客户端发送一个 HTTP 错误响应。如果已经调用过 flush 方法,则不可能再发送错误码,便简单地结束响应。如果仅仅输出了响应信息,但还没有 flush 到客户端,则会放弃输出的响应信息,而使用错误页面代替。方法中调用 write_error 方法便是输出错误页面,通过在 RequestHandler 子类中覆写这个方法可以自定义错误页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def send_error(self, status_code=500, **kwargs):
if self._headers_written:
gen_log.error("Cannot send error response after headers written")
if not self._finished:
self.finish()
return
self.clear()

reason = None
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()

write_error method

用户可以覆写 write_error 方法来输出自定义的错误页面。 在方法中可以调用诸如 write, render, set_header 等一系列辅助响应输出的方法。如果错误是由未捕获的异常(包括 HTTP Error)导致的,那么一个 exc_info 元组应包含在关键字参数 kwargs["exc_info"] 中。另外需要注意的是这个异常可能个并不是 “当前异常”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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,
})