手摸手,带你用Django REST Framework撸接口系列十二(异常处理篇)

异常… 让错误处理清晰地位于程序架构的中心位置或高层位置。REST framework提供了异常处理,我们可以自定义异常处理函数。

异常

REST framework 视图中的异常处理

REST framework 的视图处理各种异常,并返回适当的错误响应。

需要处理的异常情况有:

  • 在 REST framework 内引发的 APIException的子类。
  • Django 的 Http404 异常。
  • Django 的 PermissionDenied 异常。

在每种情况下,REST framework 都会返回一个带有适当状态码和内容类型的响应。响应的主体将包含有关错误性质的其他细节。

大多数错误响应将包含响应正文中的关键 detail

例如,以下请求:

1
2
DELETE http://api.example.com/foo/bar HTTP/1.1
Accept: application/json

可能会收到错误响应,指出在该资源上不允许使用 DELETE 方法:

1
2
3
4
5
HTTP/1.1 405 Method Not Allowed
Content-Type: application/json
Content-Length: 42

{"detail": "Method 'DELETE' not allowed."}

验证错误的处理方式稍有不同,但都是将字段名称作为响应中的关键字。如果验证错误不是特定于某个字段的,那么它将使用 “non_field_errors” 键,或者为 NON_FIELD_ERRORS_KEY setting 设置的字符串值。

示例验证错误可能如下所示:

1
2
3
4
5
HTTP/1.1 400 Bad Request
Content-Type: application/json
Content-Length: 94

{"amount": ["A valid integer is required."], "description": ["This field may not be blank."]}

自定义异常处理

你可以通过创建处理函数来实现自定义异常处理,该函数将 API 视图中引发的异常转换为响应对象。这使你可以控制 API 错误响应的样式。

该函数必须带有一对参数,第一个是要处理的异常,第二个是包含任何额外上下文(例如当前正在处理的视图)的字典。异常处理函数应该返回一个 Response 对象,或者如果无法处理异常,则返回 None。如果处理程序返回 None,那么异常将被重新抛出,Django 将返回一个标准的 HTTP 500 ‘server error’ 响应。

例如,你可能希望确保所有错误响应都包含响应正文中的 HTTP 状态码,如下所示:

1
2
3
4
5
HTTP/1.1 405 Method Not Allowed
Content-Type: application/json
Content-Length: 62

{"status_code": 405, "detail": "Method 'DELETE' not allowed."}

为了改变响应的风格,你可以编写下面的自定义异常处理程序:

1
2
3
4
5
6
7
8
9
10
11
12
from rest_framework.views import exception_handler

def custom_exception_handler(exc, context):
# Call REST framework's default exception handler first,
# to get the standard error response.
response = exception_handler(exc, context)

# Now add the HTTP status code to the response.
if response is not None:
response.data['status_code'] = response.status_code

return response

context 参数不被默认处理程序使用,但是如果异常处理程序需要更多信息,例如当前正在处理的视图(可以作为 context['view'] 访问),则该参数可能很有用。

异常处理程序还必须使用 EXCEPTION_HANDLER setting key 在你的设置中进行配置。例如:

1
2
3
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'my_project.my_app.utils.custom_exception_handler'
}

如果未指定,则 'EXCEPTION_HANDLER' setting 默认为由 REST framework 提供的标准异常处理程序:

1
2
3
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler'
}

请注意,异常处理程序只会根据由异常产生的响应调用。它不会用于视图直接返回的任何响应,例如在序列化验证失败时通用视图返回的 HTTP_400_BAD_REQUEST 响应。


API 参考

APIException

签名: APIException()

APIView 类或 @api_view 中引发的所有异常的基类。

要自定义异常,请继承 APIException,并在该类上设置 .status_code.default_detaildefault_code 属性。

例如,如果你的 API 依赖于可能无法访问的第三方服务,则可能需要为 “503 Service Unavailable” HTTP 响应码封装异常。你可以这样做:

1
2
3
4
5
6
from rest_framework.exceptions import APIException

class ServiceUnavailable(APIException):
status_code = 503
default_detail = 'Service temporarily unavailable, try again later.'
default_code = 'service_unavailable'

检查 API 异常

有许多不同的属性可用于检查 API 异常的状态。你可以使用它们为你的项目构建自定义异常处理。

可用的属性和方法有:

  • .detail - 返回错误的文本描述。
  • .get_codes() - 返回错误的代码标识符。
  • .get_full_details() - 返回文本描述和代码标识符。

在大多数情况下,错误详情将是一个简单的 item:

1
2
3
4
5
6
>>> print(exc.detail)
You do not have permission to perform this action.
>>> print(exc.get_codes())
permission_denied
>>> print(exc.get_full_details())
{'message':'You do not have permission to perform this action.','code':'permission_denied'}

在验证错误的情况下,错误详情将是 item 列表或字典:

1
2
3
4
5
6
>>> print(exc.detail)
{"name":"This field is required.","age":"A valid integer is required."}
>>> print(exc.get_codes())
{"name":"required","age":"invalid"}
>>> print(exc.get_full_details())
{"name":{"message":"This field is required.","code":"required"},"age":{"message":"A valid integer is required.","code":"invalid"}}

ParseError

签名: ParseError(detail=None, code=None)

在访问 request.data 时包含格式错误的数据则会引发此异常。

默认情况下,此异常会导致 HTTP 状态码 “400 Bad Request” 的响应。

AuthenticationFailed

签名: AuthenticationFailed(detail=None, code=None)

当传入的请求包含不正确的身份验证时引发。

默认情况下,此异常会导致 HTTP 状态码 “401 Unauthenticated” 的响应,但也可能会导致 “403 Forbidden” 响应,具体取决于所使用的身份验证方案。

NotAuthenticated

签名: NotAuthenticated(detail=None, code=None)

当未经身份验证的请求未通过权限检查时引发。

默认情况下,此异常会导致 HTTP 状态码 “401 Unauthenticated” 的响应,但也可能会导致 “403 Forbidden” 响应,具体取决于所使用的身份验证方案。

PermissionDenied

签名: PermissionDenied(detail=None, code=None)

当经过身份验证的请求未通过权限检查时引发。

默认情况下,此异常会导致 HTTP 状态码 “403 Forbidden” 的响应。

NotFound

签名: NotFound(detail=None, code=None)

当资源不存在于给定的 URL 时引发。这个异常相当于标准的 Http404 Django 异常。

默认情况下,此异常会导致 HTTP 状态码为 “404 Not Found” 的响应。

MethodNotAllowed

签名: MethodNotAllowed(method, detail=None, code=None)

当请求发生时,找不到视图上对应的处理方法时引发。

默认情况下,此异常会导致 HTTP 状态码为 “405 Method Not Allowed” 的响应。

NotAcceptable

签名: NotAcceptable(detail=None, code=None)

当请求发生时,任何可用渲染器都不符合 Accept header 时引发。

默认情况下,此异常会导致 HTTP 状态码为 “406 Not Acceptable” 的响应。

UnsupportedMediaType

签名: UnsupportedMediaType(media_type, detail=None, code=None)

如果在访问 request.data 时没有可以处理请求数据的内容类型的解析器,就会引发。

默认情况下,此异常会导致 HTTP 状态码 “415 Unsupported Media Type” 的响应。

Throttled

签名: Throttled(wait=None, detail=None, code=None)

传入的请求未通过限流检查时引发。

默认情况下,此异常会导致 HTTP 状态码 “429 Too Many Requests” 的响应。

ValidationError

签名: ValidationError(detail, code=None)

ValidationError 异常与其他 APIException 类略有不同:

  • detail 参数是必需的,不是可选的。
  • detail 参数可以是错误详情列表或字典,也可以是嵌套的数据结构。
  • 按照惯例,你应该导入 serializers 模块并使用完全限定的 ValidationError 样式,以区别于 Django 内置的验证错误。例如: raise serializers.ValidationError('This field must be an integer value.')

ValidationError 类应该用于序列化类和字段验证以及验证器类。使用 raise_exception 关键字参数调用 serializer.is_valid 时也会引发此问题:

1
serializer.is_valid(raise_exception=True)

通用视图使用 raise_exception=True 标志,意味着你可以在 API 中全局覆盖验证错误响应的样式。为此,请使用自定义异常处理程序,如上所述。

默认情况下,此异常会导致 HTTP 状态码 “400 Bad Request” 的响应。

自定义响应格式

之前的文章手摸手,带你用Django REST Framework撸接口系列四(渲染器篇)中,我们讲了如何定义渲染器,今天又讲了如何自定义异常处理,那么我们能否将其运用在实际项目中呢?

其实观察drf默认的响应格式来看,如果如渲染数据直接渲染出来的是一个字典如:

如果渲染的是异常呢?

虽然接口的目的是达到了,但是对于前端来说却是有一定问题的,比如:我无法通过返回结果来确定到底返回的是成功的数据还是错误的异常,你不能让前端通过一个detail来判断吧?

通常看其他项目的响应结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"msg": "success",
"code": 1,
"data": {
"id": 23,
"full_status": "发布",
"title": "iPhone 12 mini拖后腿!苹果将赔偿三星巨额损失",
"body": "近日,产业链传来消息称,苹果将会在2021年上半年减产约20%的iPhone,被砍掉的生产订单主要是iPhone 12 mini。 数据显示,iPhone 12 mini是该系列4款机型中市场表现最差的机型,销量远低于其他3款机型。 值得一提的是,iPhone 12 mini销量低迷也影响了三星小尺寸OLED面板的出货量。有外媒称,三星小尺寸OLED面板出货量下滑了9%至4500万块。",
"status": "p",
"created_time": "2021-03-26T02:15:01.174799Z",
"author": 1
}
}

1
2
3
4
5
{
"msg": "请求超过了限速。 Expected available in 26 seconds.",
"code": 0,
"data": ""
}

这样是不是对于前端来说更友好一些,前端可以通过固定的code就可以判断出成功还是异常,甚至可以判断是何种错误,那这种方式要如何做呢?

定义自定义异常处理

在这之前我们下看下没有使用异常处理默认响应结果是什么样的

超过请求频率的:

请求参数有误的:

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import exception_handler


def custom_exception_handler(exc, context):
"""
格式化异常输出
有多条异常的取第一条,且关键字为message
"""
# 先调用Drf默认的异常处理程序,取得标准的错误响应
response = exception_handler(exc, context)

# 获取错误列表中的第一个错误信息
# 如:即该字典的第一个元素组成的元组即:('title', [ErrorDetail(string='禁止有脏字', code='invalid'), ErrorDetail(string='请确保这个字段不能超过 5 个字符。', code='max_length')])
"""
{
'title': [
ErrorDetail(string='禁止有脏字', code='invalid'),
ErrorDetail(string='请确保这个字段不能超过 5 个字符。', code='max_length')
],
'body': [
ErrorDetail(string='禁止有脏字', code='invalid')
]
}
"""
# 或:('detail', ErrorDetail(string='请求超过了限速。 Expected available in 56 seconds.', code='throttled'))
"""
{
'detail': ErrorDetail(string='请求超过了限速。 Expected available in 56 seconds.', code='throttled')
}
"""
if response is None:
return Response({
'message': '服务器错误:%s' % exc
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR, exception=True)
else:
# 获取首条错误信息
first_err_tuple = list(response.data.items())[0]
# 该字段只有一条规则没有通过,直接取出该错误信息
if isinstance(first_err_tuple[1], str):
exc = first_err_tuple[1]
else:
# 说明该字段有多条验证规则不同过,只取第一条验证错误信息
exc = first_err_tuple[0] + first_err_tuple[1][0]

return Response({
'message': exc
}, status=response.status_code, exception=True)
return response

记得修改settings

1
2
3
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'api.exception.simple.custom_exception_handler',
}

再次触发异常:

从结果来看,

单条异常将响应结果中的detail编程了message

多条异常除了将响应结果中的detail变为了message外,还将多条异常结果变为第一条异常

自定义渲染类

自定义异常处理方法只是更改了异常的数据响应格式,对于正确的数据并没有任何改变,接下来我们试图改变渲染的格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from rest_framework.renderers import JSONRenderer


class CustomRenderer(JSONRenderer):
"""自定义渲染结果格式"""
# 重构render方法
def render(self, data, accepted_media_type=None, renderer_context=None):
if renderer_context:
if isinstance(data, dict):
msg = data.pop('message', 'success')
code = data.pop('code', renderer_context['response'].status_code)
else:
msg = 'success'
code = renderer_context['response'].status_code
ret = {
'msg': msg,
'code': code,
'data': data
}
return super().render(ret, accepted_media_type, renderer_context)
return super().render(data, accepted_media_type, renderer_context)

settings配置该渲染器

1
2
3
4
5
6
7
8
REST_FRAMEWORK = {
# 自定义异常处理函数
'EXCEPTION_HANDLER': 'api.exception.simple.custom_exception_handler',
# 使用自定义渲染类
'DEFAULT_RENDERER_CLASSES': (
'api.exception.simple.CustomRenderer',
),
}

预览结果:

综合以上便完成了我们的需求了。