Django视图函数捕捉异常

前言


不存在没有BUG的代码,也不存在不会出现异常的视图函数。当Django视图函数中的代码出现异常时,访问这个URL的用户就会收到状态码为500 Internal Server Error的HTTP 响应。如何捕捉视图函数里出现的异常,这是本篇文章要讨论的问题。

注:本文所使用的运行环境为uWSGI 2.0.18+Python3.6+Django 2.2

在uWSGI的日志中显示Traceback

这种方法算是一种比较消极的异常捕捉方式——仅将Traceback异常信息记录在uWSGI的日志中,不做进一步处理。日志可以用于日后分析,但用户收到的仍然是500 Internal Server Error响应。而这种捕捉方式也是默认启用的,只要所使用的Django版本高于或等于1.9就行。例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# django_test/urls.py
from django.urls import path
from .views import index

urlpatterns = [
path('index', index),
]

# django_test/views.py
from django.http.response import HttpResponse

def index(request):
1 / 0
return HttpResponse(b'hello world')

配置好路由和视图函数并用uWSGI启动应用后,访问/index页面就会触发ZeroDivisionError异常,该异常的Traceback信息也会输出至uWSGI的日志中,效果如下:

1
2
3
4
5
6
7
8
9
10
$ uwsgi --module django_test.wsgi --http 0.0.0.0:5000
*** Starting uWSGI 2.0.18 (64bit) on [Thu May 23 16:27:42 2019] ***
......
spawned uWSGI worker 1 (and the only) (pid: 13888, cores: 1)
Traceback (most recent call last):
......
File "./django_test/views.py", line 3, in index
1 / 0
ZeroDivisionError: division by zero
[pid: 13888|app: 0|req: 1/1] 127.0.0.1 () {40 vars in 964 bytes} [Thu May 23 08:27:44 2019] GET /index => generated 55641 bytes in 47 msecs (HTTP/1.1 500) 1 headers in 63 bytes (1 switches on core 0)

用函数装饰器捕捉异常

Django中的视图函数只是一个普通的函数(废话),这就意味着我们可以用一个能够捕捉异常的函数装饰器来包裹这个视图函数,例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# django_test/views.py
from django.http.response import HttpResponse

def catch_exc(func):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as ex:
# put your code here
# logger.error(traceback.format_exc())

# return Response or just raise
return HttpResponse(str(ex))

return wrapper


@catch_exc
def index(request):
1 / 0
return HttpResponse(b'hello world')

在例子第18行~第19行,我用catch_exc这个装饰器包裹了index这个视图函数。而在catch_exc这个装饰器内部,我用except Exception语句捕捉了index函数所有可能出现的异常。然后,我们就可以按照需要来进行各种操作了,比如:

  • traceback.format_exc()获取详细的Traceback信息;
  • 将错误信息输出至日志;
  • 通知某人;

等等等等。对异常的处理完成后,我们既可以通过raise语句让用户仍然收到500 Internal Server Error响应,也可以像上面的例子中返回一个关于异常信息的200 OK响应,还可以渲染一个提示HTML页面返回给用户。总之,Everything is under control.

用Django中间件捕捉异常

函数装饰器很好用,但装饰器只能针对单个视图函数。有没有一种应用于所有视图函数之上的异常捕捉方法?有的,这就是Django的中间件。和我上一篇文章(使用中间件对视图函数进行性能分析)不同的是,那篇文章里所使用的中间件hook为process_request&process_response,而这里使用中间件hook为process_exception,因为前者无法捕捉异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# django_test/my_middleware.py
from django.http import HttpResponse

class CatchExcMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response

def __call__(self, request):
return self.get_response(request)

def process_exception(self, request, exception):
# put your code here
# logging.error(traceback.format_exc())

# return Response or None
return HttpResponse(str(exception))

# django_test/settings.py
# ......
MIDDLEWARE = [
# ......
'django_test.my_middleware.CatchExcMiddleware', # 启用中间件
]
# ......

和上一节的装饰器类似,当视图函数出现异常时,Django就会遍历所有中间件的process_exceptionhook,包括例子里的。在这个process_exception方法里,我们同样可以完成上一节提到的各种操作,操作完成后可以通过return None语句让用户仍然收到500 Internal Server Error响应,也可以像上面的例子中返回一个关于异常信息的200 OK响应……这里就不再重复了。

后记

其实在非DEBUG模式(settings.pyDEBUG = False)下,Django可以将视图函数里出现的异常信息以电子邮件的形式发送给ADMINS里配置的邮箱地址。这个功能需要配置邮箱参数、LOGGING,这里就不详细展开了,有兴趣的话可以查看Django2.2官方文档(Error reporting)或搜索相关教程。


Django视图函数捕捉异常
https://www.yooo.ltd/2019/05/23/Django视图函数捕捉异常/
作者
OrangeWolf
发布于
2019年5月23日
许可协议