68. drf之全局异常处理

1. 回顾

继承APIView的视图类,运行步骤:

  1.去除csrf认证

  2.包装新的request

  3.运行:认证、权限、频率

      三大组件运行完,才运行视图类的函数

  4.统一处理了异常:三大组件、视图类的函数只要出现了异常,都会被统一处理

2. 引入

在前端开发中,为了便于处理后端报错,通常需要后端返回统一的格式。
比如根据错误码展示不同的提示信息给用户。

{code:999,msg:'系统异常,请联系系统管理员'}

// 其中code表示错误码,msg表示错误信息。

只要有三大组件,视图类的方法出了异常,都会运行一个函数:

# 全局异常处理
# 'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler',

在后端开发中,无论是视图类的方法出现异常,还是其他地方出现异常,都可以通过全局异常处理来进行统一的处理。
例如,给定的Python代码片段展示了一个全局异常处理函数,它会处理所有视图类方法出现的异常。
通过配置'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler'
  当发生异常时,系统会调用该函数进行异常处理。
在该函数中,可以执行一些自定义的操作来处理异常
  比如记录日志、返回统一的错误响应等。

但是不能在源码中修改处理异常的函数
  这是因为源码是框架或库的核心部分,更改源码可能导致意想不到的后果,并且对于源码的修改在升级或维护时可能会产生冲突。
  因此,在开发过程中应尽量避免修改源码,而是通过配置或继承等方式来实现自定义的异常处理逻辑。

3. drf全局异常处理源码分析

查看APIView的源码,定位到rest_framework的views.py中

APIView类中有dispatch函数,可以通过views.py的结构找到

dispatch中如果遇到了异常,就会运行

        except Exception as exc:
            response = self.handle_exception(exc)

本质就是在运行APIView类里面的handle_exception函数

handle_exception的步骤:

exception_handler = self.get_exception_handler()  # 1.获取drf提供的异常处理函数

context = self.get_exception_handler_context()  # 2.获取context(上下文对象),这里是个字典,有当次请求的request和视图类的对象view
response = exception_handler(exc, context)  # 3.运行异常处理函数,传入了exc异常对象和上下文对象

return response  # 4.返回response对象给前端Response(data={}, headers={}),返回给前端的数据,取决于response对象中的data

查看以上步骤1中get_exception_handler的源码

    def get_exception_handler(self):
        return self.settings.EXCEPTION_HANDLER

get_exception_handler函数的本质是去rest_framework的settings.py(非项目的配置文件)中找key为EXCEPTION_HANDLER的值

# Exception handling
'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler',

EXCEPTION_HANDLER的本质就是rest_framework包下views.py文件中的exception_handler函数

exception_handler函数的作用是一旦出现了异常,包装成Response对象,最终前端展示的数据取决于Response的data属性

查看以上步骤2中get_exception_handler_context的源码

    def get_exception_handler_context(self):
        return {
            'view': self,
            'args': getattr(self, 'args', ()),
            'kwargs': getattr(self, 'kwargs', {}),
            'request': getattr(self, 'request', None)
        }

get_exception_handler_context的本质是视图函数上的参数

可以在app01的views.py中导入exception_handler,从而查看这个函数的源码

from rest_framework.views import exception_handler

def exception_handler(exc, context):
    if isinstance(exc, Http404):
        exc = exceptions.NotFound(*(exc.args))
    elif isinstance(exc, PermissionDenied):
        exc = exceptions.PermissionDenied(*(exc.args))
   
# 所有drf的异常类,都继承自APIException,
# 通过from rest_framework.exceptions导入,包含ValidationError、PermissionDenied、AuthenticationFailed
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 # 如果错误类型不在rest_framework.exceptions中,返回NOne,前端会报错崩溃

其它报错演示:

app01的urls.py

from django.urls import path
from .views import BookView

urlpatterns = [
    path('book/', BookView.as_view()),
]

app01的views.py

from rest_framework.views import APIView

class BookView(APIView):
    def get(self, request, *args, **kwargs):
        return None

报错原因为视图类不能返回None

image

视图类不返回None,而是返回标准格式,前端则不会报错崩溃

from rest_framework.views import APIView

class BookView(APIView):
    def get(self, request, *args, **kwargs):
        return Response({'code': 100, 'msg': '请求成功'})

image

(1)如果在视图类中抛出APIException类型的错误,则前端会展示APIException的detail形式的错误

from rest_framework.views import APIView
from rest_framework.exceptions import APIException

class BookView(APIView):
    def get(self, request, *args, **kwargs):
        raise APIException('some errors有些错误')

image

(2)如果在视图类中抛出python类型Exception的错误,则drf函数不会处理,以通过返回None的形式导致前端会报错崩溃

from rest_framework.views import APIView

class BookView(APIView):
    def get(self, request, *args, **kwargs):
        raise Exception('some errors有些错误')

image

 

总结:drf的异常处理函数exception_handler只处理了继承自rest_framework.exceptions的异常,没处理django或python的异常

为了使drf的异常处理函数exception_handler也能处理django或python的异常,需要重写这个函数,可以做到返回统一的格式  

  如果没有报错
    查询所有:{code:100,msg:'成功',results:[{},{},]}
    查询单条:{code:100,msg:'成功',result:{}}
    删除:{code:100,msg:'删除成功'}
    发送短信:{code:100,msg:'短信发送成功'}
  如果有报错:
    {code:101,msg:'发送短信失败,请稍后再试'}
    {code:103,msg:'服务器异常,请联系系统管理员'}

4. drf全局异常处理介绍

4.1 概念

DRF 全局异常处理,用于拦截视图抛出的全部异常,统一格式化返回 JSON 响应,替代框架默认的 500、404 等原生页面输出。
核心入口为 DRF 的 exception_handler 函数,自定义异常处理器需要重写该逻辑,在项目配置文件中注册,分为 DRF 内置异常、Python 原生异常、自定义业务异常三类处理场景。

运行步骤:
请求到达视图 → 抛出异常 → 调用配置的 exception_handler → 优先执行 DRF 内置异常处理 → 内置处理返回 None 时,进入自定义扩展逻辑 → 返回标准化 Response 对象。

4.2 如何使用

1. 创建异常处理工具文件,例如app01的exception.py
2. 编写自定义异常处理函数
3. 在 settings.py 中配置 REST_FRAMEWORK,指定 EXCEPTION_HANDLER 指向自定义函数
4. 可选:定义业务自定义异常类,在视图中主动抛出

4.3 参数说明

参数说明
exc 捕获到的异常实例对象,可读取 exc.detail 获取异常描述
context 上下文字典,包含 view、request 等对象,可拿到当前视图与请求信息
response 原生 exception_handler 返回结果,不为 None 代表 DRF 已经识别该异常;None 代表未识别,属于系统未知异常
EXCEPTION_HANDLER DRF 配置项,值为字符串,路径为 模块名。函数名

5. 重写异常处理函数

5.1 基础版

app01中定义exception.py

from rest_framework.views import exception_handler
from rest_framework.response import Response

def extra_exception_handler(exc, context):
    return Response({'code': 999, 'msg': '请求异常,等一下再试'})

项目配置文件settings.py中

REST_FRAMEWORK = {'EXCEPTION_HANDLER': 'app01.exception.extra_exception_handler',}

drf的异常处理组件'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler'已经由自定义的函数extra_exception_handler接管

所有异常都将由extra_exception_handler处理

(1)视图类中抛出APIException类型的错误,前端不再展示APIException的detail形式的错误,而是返回统一错误格式

from rest_framework.views import APIView
from rest_framework.exceptions import APIException

class BookView(APIView):
    def get(self, request, *args, **kwargs):
        raise APIException('some errors有些错误')

image

(2)视图类中抛出python类型Exception的错误,不再以drf函数返回None的形式导致前端会报错崩溃,而是返回统一错误格式

from rest_framework.views import APIView

class BookView(APIView):
    def get(self, request, *args, **kwargs):
        raise Exception('some errors有些错误')

image

(3)视图类中除以0的错误,不再以drf函数返回None的形式导致前端会报错崩溃,而是返回统一错误格式

from rest_framework.views import APIView

class BookView(APIView):
    def get(self, request, *args, **kwargs):
        1/0
        return Response({'code': 100, 'msg': '请求成功'})

image

自定义的异常处理函数extra_exception_handler返回的都是统一错误格式,没有对异常类型加以区分,是drf异常还是python异常

5.2 区分异常类型

[1] 代码

在自定义的异常处理函数中,区分是drf的异常,还是python的异常

app01的exception.py

from rest_framework.views import exception_handler
from rest_framework.response import Response

def extra_exception_handler(exc, context):
    response = exception_handler(exc, context)  # 按原来的handle_exception写法
    # drf内置的exception_handler函数捕获到drf类型的异常会返回Response对象,Python类型异常exception_handler会直接返回None,不会生成response实例。
    if response:  # 有值,说明是drf异常
        # 取出detail,返回给前端
        # 判断response.data是列表还是字典
        if isinstance(response.data, list):
            msg = response.data[0]
        elif isinstance(response.data, dict):
            msg = response.data.get('detail', None) or 'drf的detail没有值,等一下再试'  # 取不到detail的值时,or之后的字符串给None赋值
        else:
            msg = '未知错误'
        return Response({'code': 666, 'msg': f'drf请求异常[{msg}]'})
    else:  # 没有值,是python的异常
        msg = str(exc)  # 从错误实例exc获取  调用异常实例的__str__魔术方法,提取异常的描述字符串。
        return Response({'code': 999, 'msg': f'python请求异常[{msg}]'})

(1)视图类中抛出drf的APIException类型的错误,返回drf请求异常

from rest_framework.views import APIView
from rest_framework.exceptions import APIException

class BookView(APIView):
    def get(self, request, *args, **kwargs):
        raise APIException('some errors有些错误')

image

(2)视图类中抛出python类型Exception的错误,返回python请求异常

from rest_framework.views import APIView

class BookView(APIView):
    def get(self, request, *args, **kwargs):
        raise Exception('some errors有些错误')

image

(3)视图类中除以0的错误,返回python请求异常

from rest_framework.views import APIView
from rest_framework.response import Response

class BookView(APIView):
    def get(self, request, *args, **kwargs):
        1 / 0
        return Response({'code': 100, 'msg': '请求成功'})

image

[2] 补充:response.data

(1) 概念

response.data 是 DRF 内置 exception_handler 组装出来的错误数据。
大部分 DRF 校验失败场景输出字典,少部分字段校验会输出列表,还存在字符串类型。
response.data 的类型,由抛出的 APIException 内部 detail 属性决定,也由序列化器校验报错的生成逻辑决定。

(2) 产生字典的场景

绝大多数 DRF 异常,response.data 为字典。
典型来源:

1. 主动抛出 APIException、AuthenticationFailed、PermissionDenied。detail 为字符串。
输出:{"detail":"错误文本"}
2. Django Http404、PermissionDenied,被 DRF 适配包装。
输出:{"detail":"Not found"}

示例 data:

{"detail":"认证失败"}

(3) 产生列表的场景

只出现在序列化器字段校验报错,序列化器校验失败抛出 ValidationError。
当序列化器对单个字段校验失败,ValidationError 的 detail 是字典,value 为列表。
exception_handler 处理 ValidationError 时,直接把 detail 赋值给 response.data。

{"username":["该用户名已存在"]}

(4) response.data 全部可能类型

类型触发条件示例
dict 绝大多数异常,APIException、404、权限拒绝、序列化器字段校验 {"detail":"xxx"},{"age":["数字超出范围"]}
list 手动 raise ValidationError (字符串列表) ["参数 1 错误","参数 2 错误"]
str 手动 raise ValidationError ("字符串"),detail 直接传字符串 "输入格式错误"

(5) rest_framework.views.exception_handler 源码片段逻辑简化

1. 如果异常是 APIException,取 exc.detail,赋值给 response.data。
2. detail 可以是字符串、列表、字典,完全由抛异常时传入的值决定。
3. ValidationError 继承 APIException,序列化器校验失败会构造 ValidationError,detail 结构随校验结果变化。

5.3 将错误码分得更细

[1] 代码

app01的exception.py

from rest_framework.response import Response
from rest_framework.exceptions import ValidationError, Throttled, AuthenticationFailed

def extra_exception_handler(exc, context):  # DRF原生异常处理器接收两个参数,即使没有用到context也必须写,否则报错
    if isinstance(exc, ValidationError):
        data = {'code': 101, 'msg': f'字段校验失败'}
    elif isinstance(exc, AuthenticationFailed):
        data = {'code': 102, 'msg': '登录认证失败'}
    elif isinstance(exc, Throttled):
        data = {'code': 103, 'msg': '超过频率限制'}
    else:
        data = {'code': 666, 'msg': str(exc)}  # 调用异常实例的__str__魔术方法,提取异常的描述字符串。
    return Response(data=data)

[2] 原理

定义自定义异常处理函数,DRF 全局异常处理器固定要求两个入参。
exc:实际捕获到的异常实例对象,用来判断是什么类型的错误。
context:上下文字典,包含当前 request、view 视图类对象等运行环境信息,本示例没有使用该参数。
  isinstance 判断捕获到的异常实例,是否属于 ValidationError 类型。
  原生 ValidationError 本身携带详细字段错误信息,本代码直接覆盖,丢弃原本的字段错误详情。
  组装自定义返回字典。code 为业务错误码,msg 对外展示错误提示文本。
把组装好的字典传入 Response,作为接口返回值。
项目的配置文件中指定该函数作为全局异常处理函数。REST_FRAMEWORK = {'EXCEPTION_HANDLER': 'app01.exception.extra_exception_handler',}

[3] 运行链路简述

  1. 接口请求进入 APIView 视图运行。
  2. 视图执行过程抛出异常。
  3. APIView 顶层捕获异常,读取 REST_FRAMEWORK 的 EXCEPTION_HANDLER 配置。
  4. 调用 extra_exception_handler,传入 exc 异常对象、context 上下文。
  5. 函数内部判断异常类型,组装自定义 data,返回 Response 实例。
  6. Django 将该 Response 转为 HTTP JSON 响应返回客户端。

[4] 测试

视图类中抛出drf的AuthenticationFailed类型的错误

app01的views.py

from rest_framework.views import APIView
from rest_framework.exceptions import ValidationError, AuthenticationFailed

class BookView(APIView):
    def get(self, request, *args, **kwargs):
        raise AuthenticationFailed('some errors有些错误')

原生AuthenticationFailed本身具有详细字段错误信息,自定义全局异常处理函数直接覆盖,丢弃原本的字段错误详情('some errors有些错误')。

image

如果需要使用原生AuthenticationFailed的字段错误信息,通过exc.detail获取

from rest_framework.response import Response
from rest_framework.exceptions import ValidationError, Throttled, AuthenticationFailed

def extra_exception_handler(exc, context):  # DRF原生异常处理器接收两个参数,即使没有用到context也必须写,否则报错
    if isinstance(exc, ValidationError):
        data = {'code': 101, 'msg': exc.detail}
    elif isinstance(exc, AuthenticationFailed):
        data = {'code': 102, 'msg': exc.detail}
    elif isinstance(exc, Throttled):
        data = {'code': 103, 'msg': exc.detail}
    else:
        data = {'code': 666, 'msg': str(exc)}  # 调用异常实例的__str__魔术方法,提取异常的描述字符串。
    return Response(data=data)

image

[5] APIException报错测试与记录日志

app01的exception.py

from rest_framework.response import Response
from rest_framework.exceptions import ValidationError, Throttled, AuthenticationFailed, APIException
import time

def extra_exception_handler(exc, context):  # DRF原生异常处理器接收两个参数,即使没有用到context也必须写,否则报错
    if isinstance(exc, ValidationError):
        data = {'code': 101, 'msg': exc.detail}
    elif isinstance(exc, AuthenticationFailed):
        data = {'code': 102, 'msg': exc.detail}
    elif isinstance(exc, Throttled):
        data = {'code': 103, 'msg': exc.detail}
    elif isinstance(exc, ZeroDivisionError):
        data = {'code': 104, 'msg': '不能除以0'}
    elif isinstance(exc, APIException):
        data = {'code': 105, 'msg': exc.detail}
    else:
        data = {'code': 666, 'msg': str(exc)}  # 调用异常实例的__str__魔术方法,提取异常的描述字符串。
    # 运行到这里,说明出现异常了,需要记录日志
    request = context.get('request')
    view = context.get('view')
    print(f"""运行异常:
    错误原因是:[{str(exc)}]
    时间是:[{time.strftime('%Y-%m-%d %H:%M:%S')}]
    请求地址是:[{request.get_full_path()}]
    请求方式是:[{request.method}]
    用户是:[{request.user.username or '未登录用户'}]
    出错的视图类是:[{str(view)}]
    """)
    return Response(data=data)

 app01的views.py

from rest_framework.response import Response
from rest_framework.views import APIView
class BookView(APIView):
    def get(self, request, *args, **kwargs):
        return Response({'code': 999, 'msg': '请求成功'})

当发送post请求时,触发APIException报错

image

打印日志

image

6. isinstance与issubclass

isinstance()函数和issubclass()函数是Python中的两个内置函数,用于判断对象与类之间的关系。

[1] isinstance
isinstance()函数用于检查一个对象是否是一个类的实例。
它接受两个参数
  第一个参数是待检查的对象
  第二个参数是类或类型。
如果对象是第二个参数指定的类或类型的实例
  则返回True;
  否则返回False。

例如:

class Person:
    pass

person = Person()
print(isinstance(person, Person))  # 输出:True
print(isinstance(person, object))  # 输出:True
print(isinstance(person, str))     # 输出:False

在这个例子中,通过isinstance()函数可以判断person对象是否是Person类的实例。
同时,由于所有的类都继承自object类,所以person对象也被认为是object类的实例。

[2] issubclass
issubclass()函数用于检查一个类是否是另一个类的子类。
它接受两个参数
  第一个参数是待检查的类
  第二个参数是基类或超类。
如果第一个参数是第二个参数指定的类或类型的子类
  则返回True;
  否则返回False。
例如:

class Animal:
    pass

class Dog(Animal):
    pass

print(issubclass(Dog, Animal))   # 输出:True
print(issubclass(Dog, object))   # 输出:True
print(issubclass(Dog, str))      # 输出:False

在这个例子中,通过issubclass()函数可以判断Dog类是否是Animal类的子类。
  同时由于所有的类都是object类的子类,所以Dog类也被认为是object类的子类。
这就是isinstance()函数和issubclass()函数的详解。
  通过使用这两个函数,我们可以判断对象与类之间的关系,从而进行相应的处理。

 

posted @ 2026-09-14 06:05  pythondjango  阅读(4)  评论(0)    收藏  举报