Django与DRF结合的全局异常处理方案详解
本文详解Django与DRF结合时的全局异常处理方案。通过自定义exception_handler拦截DRF异常并统一返回格式,同时利用Django中间件处理非DRF请求。方案特别保留了对/admin路径的原始处理行为,确保后台管理功能不受影响,并在生产环境提供友好的错误提示。
前言
Django 与 DRF(django rest framawork) 的组合在 Python 后端开发中十分常见。为了实现统一的异常处理,我们需要一套完整的解决方案:在开发环境中展示详细的异常堆栈信息,而在生产环境中则向用户返回友好的错误提示。本文旨在提供一种统一的全局异常处理方案,供读者参考使用。
实现的目标
若不使用 DRF,仅需在 Django 中添加一个中间件即可解决全局异常处理问题。然而,DRF 会自行处理部分异常并自动返回给客户端,因此我们需要协调 Django 与 DRF 之间的异常处理策略,避免冲突。
此外,我们希望继续使用 Django 的 admin 后台进行数据查看和修改,因此必须保留 admin 原有的内部异常处理行为,确保后台管理功能正常运作。
本文设定的目标如下:
- 保留 Django 自带的 admin 异常处理行为
- 拦截 DRF 产生的异常,并执行全局异常处理逻辑
- 拦截除 DRF 异常之外的其他 Django 异常,并执行全局异常处理逻辑
DRF 全局异常拦截的解决思路
首先,DRF 的所有异常均继承自 APIException 类。当 DRF 抛出异常时,会被 exception_handler 这个异常处理函数拦截(该函数的具体位置位于 /python3.7/site-packages/rest_framework/views.py 中)。
我们进一步查看该函数的源码实现:
def exception_handler(exc, context):
"""
Returns the response that should be used for any given exception.
By default we handle the REST framework `APIException`, and also
Django's built-in `Http404` and `PermissionDenied` exceptions.
Any unhandled exceptions may return `None`, which will cause a 500 error
to be raised.
"""
if isinstance(exc, Http404):
exc = exceptions.NotFound()
elif isinstance(exc, PermissionDenied):
exc = exceptions.PermissionDenied()
if isinstance(exc, exceptions.APIException):
headers = {}
if getattr(exc, 'auth_header', None):
headers['WWW-Authenticate'] = exc.auth_header
if getattr(exc, 'wait', None):
headers['Retry-After'] = '%d' % exc.wait
if isinstance(exc.detail, (list, dict)):
data = exc.detail
else:
data = {'detail': exc.detail}
set_rollback()
return Response(data, status=exc.status_code, headers=headers)
return None
通过该函数的文档签名可知,DRF 会处理所有继承自 APIException 的异常类,同时还会额外处理 Django 内置的 Http404 和 PermissionDenied 异常,并将处理后的结果返回给前端。
如果异常不在上述处理范围内,该函数将返回 None,此时 Django 会抛出一个 500 服务器错误异常。
DRF 支持单独配置异常处理函数,因此第一步是在 settings 文件中指定自定义异常处理函数的路径:
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'server.exception.exception_globe.globe_exception_handler'
}
随后,我们定义自己的异常处理程序:
该处理程序的逻辑分为三步:第一步,调用 DRF 默认的异常处理函数;第二步,对 DRF 拦截的异常进行自定义处理;第三步,将其他未处理的异常抛给 Django 系统处理。
def globe_exception_handler(exc, context):
"""
Below is the global exception handler of drf
Http404 / PermissionDenied / APIException
"""
# Call REST framework's default exception handler
response = exception_handler(exc, context)
request = context['request']
# Exceptions form DRF and Django built-in `Http404` and `PermissionDenied`
if response is not None:
if isinstance(response.data, list):
msg = '; '.join(response.data)
elif isinstance(response.data, str):
msg = response.data
else:
msg = 'Sorry, we make a mistake (* ̄︶ ̄)!'
ex_data = {
"msg": msg,
"error_code": 1000,
"request": request.path
}
return JsonResponse(data=ex_data, status=response.status_code)
# Exceptions from others
# 如果 response 为 None 则直接触发上面的 ExceptionGlobeMiddleware
return response
Django 异常处理方案
从上一节的结果可知,DRF 无法处理的异常会被抛给 Django。由于 Django 支持通过定义中间件来进行全局异常处理,因此我们只需定义一个 Django 全局异常处理中间件,并将其配置到 settings 文件中的 MIDDLEWARE 数组即可。
try:
from django.utils.deprecation import MiddlewareMixin # Django 1.10.x
except ImportError:
MiddlewareMixin = object # Django 1.4.x - Django 1.9.x
class ExceptionGlobeMiddleware(MiddlewareMixin):
"""
Below is the global exception handler of django
"""
def process_exception(self, request, exception):
# 直接抛出 django admin 的异常
if str(request.path).startswith('/admin/'):
return None
# 捕获其他异常,直接返回 500
ex_data = {
"msg": "Sorry, we make a mistake (* ̄︶ ̄)!",
"error_code": 1000,
"request": request.path
}
return JsonResponse(data=ex_data, status=500)
值得注意的是,我们可以在中间件的处理函数中获取到 request 对象。通过该对象,我们可以获取用户请求的 URL,进而通过判断 URL 路径来确定哪些请求来自 Django 自带的 admin 后台。
参考代码如下:
# 直接抛出 django admin 的异常
if str(request.path).startswith('/admin/'):
return None
总结
通过上述配置,我们完成了 Django 结合 DRF 的全局异常处理,并成功保留了 Django 自带 admin 的异常处理策略。
在实际生产环境中,我们可以通过环境变量来有选择地打印和记录日志。
本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注本站的更多内容!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









