DRF之全局异常处理、接口文档书写
一、全局异常处理
1、drf默认异常处理源码分析
在DRF中,继承APIView后,它的执行流程是首先去除了所有请求的csrf认证,然后把视图类的request对象变成了新的request对象,新的reqeust对象是DRF的,但是以前Django的request对象用起来是一样的,同时把新的reqeust对象放到了视图类的对象中,然后在执行视图类的方法之前,又执行了三大认证。最后就是在执行三大认证或视图类方法的过程中只要报错了,就会被全局异常捕获处理。而在这里我们要说的就是这个DRF执行流程中最后一个环节,全局异常处理。
- 只要三大认证,视图类的方法出了异常,都会执行一个函数:
rest_framework.views import exception_handler
- 查看dispatch源码:如下
def dispatch(self, request, *args, **kwargs): self.args = args self.kwargs = kwargs request = self.initialize_request(request, *args, **kwargs) self.request = request self.headers = self.default_response_headers # deprecate? try: # 三大认证执行 self.initial(request, *args, **kwargs) # 视图类的方法开始执行 if request.method.lower() in self.http_method_names: handler = getattr(self, request.method.lower(), self.http_method_not_allowed) else: handler = self.http_method_not_allowed response = handler(request, *args, **kwargs) # 视图类方法结束执行 # 如果三大认证或视图类方法报错,执行全局异常处理捕获 except Exception as exc: response = self.handle_exception(exc) ''' 这里self是视图类的对象,现在在它的父类APIView里面,所以我们从APIView中找找有没有 这个handle_exception方法,另外这个括号里面的exc就是异常对象 这个handle_exception方法也是我们主要分析的方法 ''' self.response = self.finalize_response(request, response, *args, **kwargs) return self.response
- 执行handle_exception 方法时就会执行方法中
exception_handler = self.get_exception_handler()
这一行代码,但 - 是这一行代码就是前面
'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler’
中的rest_framework.views.exception_handle
函数
# 在APIView里面找到了handle_exception方法 def handle_exception(self, exc): # 如果没有认证通过,http响应状态码变成403,并且往响应头中放入了数据 if isinstance(exc, (exceptions.NotAuthenticated, exceptions.AuthenticationFailed)): # WWW-Authenticate header for 401 responses, else coerce to 403 # 如果异常是 NotAuthenticated 或 AuthenticationFailed,处理授权相关的逻辑 auth_header = self.get_authenticate_header(self.request) # 如果存在认证头,将其添加到异常中 if auth_header: exc.auth_header = auth_header else: # 如果不存在认证头,将状态码更改为 403 FORBIDDEN exc.status_code = status.HTTP_403_FORBIDDEN # 这里的exception_handler就是我们自定义异常处理重写的函数 ''' self是视图类对象,已知视图类对象中没有get_excpetion_handler这个方法,然后去父类APIView中查找到了 def get_exception_handler(self): return self.settings.EXCEPTION_HANDLER 可以看到,它是去配置文件中拿这个EXCEPTION_HANDLER了,这也是为什么我们自定义的异常处理需要配置,如果在配置文件中没有拿到,就会去DRF的配置文件中拿到它 'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler' ''' exception_handler = self.get_exception_handler() # 获取异常处理上下文 context = self.get_exception_handler_context() ''' 这里的exception_handler就是我们自定义异常处理重写的函数,这也就是为什么我们需要传两个参数的原因 exc就是错误对象,context就是上下文,它是一个对象,并且里面存着当次请求的request和视图类 这个response就是最后返回的异常结果,这就是为什么我们重写后最后需要返回一个响应对象 ''' response = exception_handler(exc, context) ''' 如果响应对象是None就是抛出异常,就不会捕获,直接返回给前端,所以这个全局异常只能捕获DRF的异常 这也是为什么我们需要重写,就是为了返回全局异常无法捕获的异常 ''' if response is None: self.raise_uncaught_exception(exc) ''' 也可以看一下这个方法,还是一样self是视图类对象,然后在父类APIView中找到了这个方法 def raise_uncaught_exception(self, exc): if settings.DEBUG: 在调试模式下处理未捕获的异常 request = self.request 获取当次请求 请求接受的渲染格式 renderer_format = getattr(request.accepted_renderer, 'format') 然后根据渲染格式渲染到页面 use_plaintext_traceback = renderer_format not in ('html', 'api', 'admin') request.force_plaintext_errors(use_plaintext_traceback) 最后将未捕获的异常直接渲染给页面 raise exc ''' # 这里将响应对象的exception属性设置为True,表示异常已被处理 response.exception = True # 最后返回处理后的响应对象 return response
-
'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler'
-
接下来我们单独去读读DRF默认配置的异常处理
def exception_handler(exc, context): # 看到DRF默认配置的全局异常处理,就可以知道为什么我们要那么配置自定义的了,其实就是照着这个写的 # 这里就是自己的异常 if isinstance(exc, Http404): exc = exceptions.NotFound() elif isinstance(exc, PermissionDenied): exc = exceptions.PermissionDenied() # 这里是DRF的所有异常,只要是DRF的异常都会走这里,因为DRF的异常都继承了APIException if isinstance(exc, exceptions.APIException): # 如果异常的详细信息是列表或字典,直接使用该信息 if isinstance(exc.detail, (list, dict)): data = exc.detail else: # 否则,将详细信息包装在 detail 键下 data = {'detail': exc.detail} # 如果想要验证drf出现异常是否会执行上面exception_handler函数的话,可以在这打印一句话 # 然后再视图类中抛出raise APIException('我出错误了') # 返回处理后的响应 ''' 所以这里有两个情况 (1) Response(data:{'detail': exc.detail}) (2) Response(data:错误信息(字典或列表)) 所以这种情况,这也是为什么我们自定义的时候,取detail时,怕取不到设置了自定义的一个异常信息或者在or一个data这样会 更好一些,例如: detail = res.data.get('detail') or res.data or "drf异常,请联系系统管理员" return Response({'code':666,'message':detail}) ''' return Response(data, status=exc.status_code, headers=headers) # 对于其他类型的异常,返回 None,让 DRF 使用默认的异常处理机制,就是不处理直接返回给前端页面 return None
2、自定义异常处理
针对上面的源码,我们也可以自定义异常处理,DRF默认的异常处理,只处理APIException及其子类的异常,处理不了的会返回None,我们可以判断异常是否属于APIException及其子类的异常,如果是则返回错误信息,如果不是,那么我们则返回服务器错误。
- 自定义异常处理
from rest_framework.views import exception_handler from rest_framework.response import Response ''' 需求: 0 执行原来的exception_handler 判断返回值是否为 None: 如果是None,说明是非drf异常,自己包装一个Response 如果不是None,说明是drf异常,包装一个Response 1 错误状态码可能会有很多类型: 通过exc 错误对象,判断具体是哪个类的对象(是什么错误)--》更细粒度区分不同的错误 991 992 993 996 998 999 2 只要程序走到common_exception_handler ,就说明有异常,通常做法 -返回给前端固定错误描述: 服务器异常,请稍后再试 -使用日志,记录错误:越详细越好 -时间,请求方式,请求的地址,客户端ip,用户id。。。。 ''' class PasswordException(Exception): def __init__(self,msg): self.msg=msg def common_exception_handler(exc, context): # 日志记录放这里即可:请求方式,请求的地址,客户端ip,用户id request=context.get('request') view=context.get('view') # 哪个视图类出的错 user_id=request.user.pk or '匿名用户' print(f'请求方式是:{request.method},请求地址是:{request.get_full_path()},客户端ip:{request.META.get("REMOTE_ADDR")},用户id:{user_id}, 错误的视图类是{view.__class__.__name__}') # 1 返回 Response:说明是drf的异常 2 返回None:说明不是drf的异常 response = exception_handler(exc, context) if response: if isinstance(response.data,dict): err=response.data.get('detail','系统错误,请联系系统管理员') elif isinstance(response.data,list): err = response.data[0] else: err ='系统错误,请联系系统管理员' response = Response({'code': 998, 'msg': f'drf的异常:{err}'}) pass else: if isinstance(exc,ZeroDivisionError): response = Response({'code': 991, 'msg': '不能除以0'}) elif isinstance(exc,Exception): response = Response({'code': 992, 'msg': 'Exception错误'}) else: # 表明是 非drf的异常 err=str(exc) response = Response({'code': 999, 'msg': f'django的异常:{err}'}) return response
- 视图类
from rest_framework.views import APIView from utils.exceptions import PasswordException from rest_framework.response import Response from rest_framework.exceptions import ValidationError, AuthenticationFailed, APIException class UserView(APIView): def get(self, request, *args, **kwargs): ''' 这个接口用来查询xx ''' # 1 drf异常,主动抛 # raise ValidationError('小伙子,出错了') # raise AuthenticationFailed('小伙子,出错了,没认证通过吖') raise APIException('错误了!!!') # 2 django的异常或python的异常 # res = 9 / 0 # raise PasswordException('ccccc') # l=[2,3,4] # print(l[9]) # raise Exception('错误') # return Response('正常返回xx')
- 配置文件
# 自定义异常配置文件中要加入 REST_FRAMEWORK = { 'EXCEPTION_HANDLER': 'app01.exceptions.common_exception_handler', }
- 总结使用步骤
#1 定义一个函数 def common_exception_handler(exc, context): # 加入日志的逻辑,错误中记录日志---> 越详细越好 ...日志逻辑... response = exception_handler(exc, context) if response: return Response(data={'code': 9998, 'msg': response.data}) else: return Response(data={'code': 9999, 'msg': '服务器异常,请联系系统管理员'}) # 2 在配置文件中配置 REST_FRAMEWORK = { 'EXCEPTION_HANDLER': 'app01.exceptions.common_exception_handler', }
3、总结
-
对于前端来讲,无论后端什么情况,前端收到的都是统一的格式
-
正常响应:
-
不正常响应:
-
出了错:{code:999,msg:错误信息}
-
-
只要三大认证,视图类的方法出了异常,都会执行一个函数:
rest_framework.views import exception_handler
-
注意:
exception_handler
- 如果异常对象是drf的APIException对象,就会返回Response
- exception_handler只处理了drf的异常,其它的异常需要我们自己处理
- 如果异常对象不是drf的APIException对象,就会返回None
-
补充:
- isinstance() 判断一个对象是不是某个类的对象 isinstance(对象,类)
- ssubclass() 判断一个类,是不是另一个类的子类
二、接口文档
1、为什么要写接口文档
在我们后端写完接口之后,应该清楚
- 地址
- 携带参数
- 请求方式
- 编码格式
- 返回数据格式
- ...
并将这些内容必须写成接口文档,方便前端参照,而前端的话在拿到接口文档,应该参考接口文档进行前端的相关工作开发。
2、接口文档展示形式
-
md、word等共享文档写接口文档
-
接口文档开放平台参考----> 微博开放平台
-
第三方平台
- 例如:showDoc 但是需要花钱
- https://www.showdoc.com.cn/item/index
-
公司自研
-
开源的接口文档平台
- Yapi:百度开源的
-
通过项目,自动生成接口文档平台
- coreapi
- drf-yasg:可以看着文档自己集成到项目里
3、接口文档规范
这要取决你所在的公司有着什么样的规范,毕竟是自己公司内部员工看的,所以需要统一
4、coreapi使用
(1)安装
pip3 install coreapi
(2)路由中配置
from rest_framework.documentation import include_docs_urls urlpatterns = [ path('docs/', include_docs_urls(title='coreapi自动生成接口文档')) ]
(3)本地配置文件中配置
REST_FRAMEWORK = { 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', }
(4)正常写视图类
-
注意:在方法中加的注释,会在接口文档中体现。在类里面但是在方法外的注释是不会体现的
-
在序列化类中写required,help_text等参数也会在接口文档中体现
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现