暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

fastapi系列之-错误异常的输出优化个人觉得比较优雅

小儿来一壶枸杞酒泡茶 2021-11-25
4184

关于fastapi处理,之前处理的方式比较不优雅,对于http错误和校验错误信息,都直接的返回对应的错误信息全部的。如果对错误处理经过加工的话 应该更加优雅一点,此处理方案来自于:fastapi_contir框架的处理。我对它基于自己的原来基础做了些小调整。

旧的错误的处理输出

最终优化后的输出

优化流程

1 定义返回的json_response

完整代码是:

from typing import Any, Dict, Optional

# 自定义返回的错误的响应体信息
# ORJSONResponse一依赖于:orjson
from fastapi.responses import JSONResponse

import time
from fastapi.encoders import jsonable_encoder
import json
import datetime
import decimal
import typing


class CJsonEncoder(json.JSONEncoder):
    def default(self, obj):
        if hasattr(obj, 'keys') and hasattr(obj, '__getitem__'):
            return dict(obj)
        if isinstance(obj, datetime.datetime):
            return obj.strftime('%Y-%m-%d %H:%M:%S')
        if isinstance(obj, datetime.date):
            return obj.strftime('%Y-%m-%d')
        if isinstance(obj, decimal.Decimal):
            return float(obj)
        if isinstance(obj, bytes):
            return str(obj, encoding='utf-8')
        return json.JSONEncoder.default(self, obj)


class ApiResponse(JSONResponse):
    # 定义返回响应码--如果不指定的话则默认都是返回200
    http_status_code = 200
    # 默认成功
    api_code = 0
    # 默认Node.如果是必选的,去掉默认值即可
    result: Optional[Dict[str, Any]] = None  # 结果可以是{} 或 []
    message = '成功'
    success = True
    timestamp = int(time.time() * 1000)

    def __init__(self, success= None, http_status_code=None, api_code=None, result=None, message=None, **options):

        if result:
            self.result = result
        if message:
            self.message = message

        if api_code:
            self.api_code = api_code

        if success != None:
            self.success = success

        if http_status_code:
            self.http_status_code = http_status_code

        # 返回内容体
        body = dict(
            message=self.message,
            code=self.api_code,
            success=self.success,
            result=self.result,
            timestamp=self.timestamp,
            # 形如request="POST v1/client/register"
            # request=request.method + ' ' + self.get_url_no_param()
        )


        # jsonable_encoder 处理不同字符串返回  比如时间戳 datatime类型的处理
        # super(ApiResponse, self).__init__(status_code=self.http_status_code, content=jsonable_encoder(body), **options)
        # jsonable_encoder 处理不同字符串返回  比如时间戳 datatime类型的处理---jsonable_encoder无法格式化
        # super(ApiResponse, self).__init__(status_code=self.http_status_code, content=jsonable_encoder(body), **options)

        if options:
            body = {**body, **options}
            [options.pop(key) for key in [itemkey for itemkey in options.keys() if itemkey not in ['content''status_code''headers''media_type''background']]]
        super(ApiResponse, self).__init__(status_code=self.http_status_code, content=body, **options)

    # 这个render会自动调用,如果这里需要特殊的处理的话,可以重写这个地方
    def render(self, content: typing.Any) -> bytes:
        return json.dumps(
            content,
            ensure_ascii=False,
            allow_nan=False,
            indent=None,
            separators=(","":"),
            cls=CJsonEncoder
        ).encode("utf-8")

class BadrequestException(ApiResponse):
    http_status_code = 400
    api_code = 10031
    result = None  # 结果可以是{} 或 []
    message = '错误的请求'
    success = False


class LimiterResException(ApiResponse):
    http_status_code = 429
    api_code = 429
    result = None  # 结果可以是{} 或 []
    message = '访问的速度过快'
    success = False


class ParameterException(ApiResponse):
    http_status_code = 400
    result = {}
    message = '参数校验错误,请检查提交的参数信息'
    api_code = 10031
    success = False


class UnauthorizedException(ApiResponse):
    http_status_code = 401
    result = {}
    message = '未经许可授权'
    api_code = 10032
    success = False



class ForbiddenException(ApiResponse):
    http_status_code = 403
    result = {}
    message = '失败!当前访问没有权限,或操作的数据没权限!'
    api_code = 10033
    success = False


class NotfoundException(ApiResponse):
    http_status_code = 404
    result = {}
    message = '访问地址不存在'
    api_code = 10034
    success = False


class MethodnotallowedException(ApiResponse):
    http_status_code = 405
    result = {}
    message = '不允许使用此方法提交访问'
    api_code = 10034
    success = False


class OtherException(ApiResponse):
    http_status_code = 800
    result = {}
    message = '未知的其他HTTPEOOER异常'
    api_code = 10034
    success = False


class InternalErrorException(ApiResponse):
    http_status_code = 200
    result = {}
    message = '程序员哥哥睡眠不足,系统崩溃了!'
    api_code = 200
    success = False


class InvalidTokenException(ApiResponse):
    http_status_code = 401
    api_code = 401
    message = '很久没操作,令牌失效'
    success = False

class ExpiredTokenException(ApiResponse):
    http_status_code = 422
    message = '很久没操作,令牌过期'
    api_code = 10050
    success = False


class FileTooLargeException(ApiResponse):
    http_status_code = 413
    api_code = 413
    result = None  # 结果可以是{} 或 []
    message = '文件体积过大'


class FileTooManyException(ApiResponse):
    http_status_code = 413
    message = '文件数量过多'
    api_code = 10120
    result = None  # 结果可以是{} 或 []


class FileExtensionException(ApiResponse):
    http_status_code = 401
    message = '文件扩展名不符合规范'
    api_code = 10121
    result = None  # 结果可以是{} 或 []


class Success(ApiResponse):
    http_status_code = 200
    api_code = 200
    result = None  # 结果可以是{} 或 []
    message = '自定义成功返回'
    success = True


class Fail(ApiResponse):
    http_status_code = 200
    api_code = 200
    result = None  # 结果可以是{} 或 []
    message = '自定义成功返回'
    success = False

PS:在上面中对于如果有其他非restur参数的需要在外层传入的话

需要添加:

  if options:
            body = {**body, **options}
            [options.pop(key) for key in [itemkey for itemkey in options.keys() if itemkey not in ['content''status_code''headers''media_type''background']]]

2 修改原来我的错误处理机制

原来的代码:

from fastapi import FastAPI, Request
from apps.response.json_response import *
from starlette.exceptions import HTTPException as StarletteHTTPException
from fastapi.exceptions import HTTPException as FastapiHTTPException
from fastapi.exceptions import RequestValidationError
from pydantic.errors import *
from apps.ext.logger import logger
import traceback
from apps.utils.singleton_helper import Singleton


@Singleton
class ApiExceptionHandler():
    def __init__(self, app=None, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if app is not None:
            self.init_app(app)

    def init_app(self, app: FastAPI):

        # @app.exception_handler(StarletteHTTPException)
        # @app.exception_handler(RequestValidationError)
        # @app.exception_handler(Exception)
        app.add_exception_handler(Exception, handler=self.all_exception_handler)
        # 捕获StarletteHTTPException返回的错误异常,如返回405的异常的时候,走的是这个地方
        app.add_exception_handler(StarletteHTTPException, handler=self.http_exception_handler)
        app.add_exception_handler(RequestValidationError, handler=self.validation_exception_handler)

    async def validation_exception_handler(self, request: Request, exc: RequestValidationError):
        # print("参数提交异常错误selfself", exc.errors()[0].get('loc'))
        # 路径参数错误
        # 判断错误类型
        if isinstance(exc.raw_errors[0].exc, IntegerError):
            pass
        elif isinstance(exc.raw_errors[0].exc, MissingError):
            pass
        return ParameterException(http_status_code=400, api_code=400, message='参数校验错误', result={
            "detail": exc.errors(),
            "body": exc.body
        })

    async def all_exception_handler(self, request: Request, exc: Exception):
        '''
        全局的捕获抛出的HTTPException异常,注意这里需要使用StarletteHTTPException的才可以
        :param request:
        :param exc:
        :return:
        '
''
        # log_msg = f"捕获到系统错误:请求路径:{request.url.path}\n错误信息:{traceback.format_exc()}"
        if isinstance(exc, StarletteHTTPException) or isinstance(exc, FastapiHTTPException):
            if exc.status_code == 405:
                return MethodnotallowedException()
            if exc.status_code == 404:
                return NotfoundException()
            elif exc.status_code == 429:
                return LimiterResException()
            elif exc.status_code == 500:
                return InternalErrorException()
            elif exc.status_code == 400:
                # 有部分的地方直接的选择使用raise的方式抛出了异常,这里也需要进程处理
                # raise HTTPException(HTTP_400_BAD_REQUEST, 'Invalid token')
                return BadrequestException(msg=exc.detail)

            return BadrequestException()
        else:
            # 其他内部的异常的错误拦截处理
            logger.exception(exc)
            traceback.print_exc()
            return InternalErrorException()

    async def http_exception_handler(self, request: Request, exc: StarletteHTTPException):
        '''
           全局的捕获抛出的HTTPException异常,注意这里需要使用StarletteHTTPException的才可以
           :param request:
           :param exc:
           :return:
           '
''
        # 这里全局监听了我们的所有的HTTP响应,包括了200 的也会尽到这里来!
        # print("撒很好收到哈搜地和撒谎的撒22222222222===========",exc)
        # log_msg = f"捕获到系统错误:请求路径:{request.url.path}\n错误信息:{traceback.format_exc()}"

        if exc.status_code == 405:
            return MethodnotallowedException()
        if exc.status_code == 404:
            return NotfoundException()
        elif exc.status_code == 429:
            return LimiterResException()
        elif exc.status_code == 500:
            return InternalErrorException()
        elif exc.status_code == 400:
            # 有部分的地方直接的选择使用raise的方式抛出了异常,这里也需要进程处理
            # raise HTTPException(HTTP_400_BAD_REQUEST, 'Invalid token')
            return BadrequestException(msg=exc.detail)


修改之后:

#!/usr/bin/evn python
# -*- coding: utf-8 -*-

from typing import Any, List, Optional
from fastapi import FastAPI, Request
from demo6.response.json_response import *
from starlette.exceptions import HTTPException as StarletteHTTPException
from fastapi.exceptions import HTTPException as FastapiHTTPException
from fastapi.exceptions import RequestValidationError
from pydantic.errors import *
import traceback


def Singleton(cls):
    _instance = {}

    def _singleton(*args, **kargs):
        if cls not in _instance:
            _instance[cls] = cls(*args, **kargs)
        return _instance[cls]

    return _singleton

def parse_error(err: Any, field_names: List, raw: bool = True) -> Optional[dict]:
    """
    Parse single error object (such as pydantic-based or fastapi-based) to dict
    :param err: Error object
    :param field_names: List of names of the field that are already processed
    :param raw: Whether this is a raw error or wrapped pydantic error
    :return: dict with name of the field (or "
__all__") and actual message
    "
""
    if isinstance(err.exc, EnumError):
        permitted_values = ", ".join([f"'{val}'" for val in err.exc.enum_values])
        message = f"Value is not a valid enumeration member; "f"permitted: {permitted_values}."
    elif isinstance(err.exc, StrRegexError):
        message = "Provided value doesn't match valid format."
    else:
        message = str(err.exc) or ""

    if not raw:
        if len(err.loc_tuple()) == 2:
            if str(err.loc_tuple()[0]) in ["body""query"]:
                name = err.loc_tuple()[1]
            else:
                name = err.loc_tuple()[0]
        elif len(err.loc_tuple()) == 1:
            if str(err.loc_tuple()[0]) == "body":
                name = "__all__"
            else:
                name = str(err.loc_tuple()[0])
        else:
            name = "__all__"
    else:
        if len(err.loc_tuple()) == 2:
            name = str(err.loc_tuple()[0])
        elif len(err.loc_tuple()) == 1:
            name = str(err.loc_tuple()[0])
        else:
            name = "__all__"

    if name in field_names:
        return None

    if message and not any(
            [message.endswith("."), message.endswith("?"), message.endswith("!")]
    ):
        message = message + "."
    message = message.capitalize()

    return {"name": name, "message": message}


def raw_errors_to_fields(raw_errors: List) -> List[dict]:
    """
    Translates list of raw errors (instances) into list of dicts with name/msg
    :param raw_errors: List with instances of raw error
    :return: List of dicts (1 dict for every raw error)
    "
""
    fields = []
    for top_err in raw_errors:
        if hasattr(top_err.exc, "raw_errors"):
            for err in top_err.exc.raw_errors:
                # This is a special case when errors happen both in request
                # handling & internal validation
                if isinstance(err, list):
                    err = err[0]
                field_err = parse_error(
                    err,
                    field_names=list(map(lambda x: x["name"], fields)),
                    raw=True,
                )
                if field_err is not None:
                    fields.append(field_err)
        else:
            field_err = parse_error(
                top_err,
                field_names=list(map(lambda x: x["name"], fields)),
                raw=False,
            )
            if field_err is not None:
                fields.append(field_err)
    return fields

@Singleton
class ApiExceptionHandler():
    def __init__(self, app=None, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if app is not None:
            self.init_app(app)

    def init_app(self, app: FastAPI):
        app.add_exception_handler(Exception, handler=self.all_exception_handler)
        # 捕获StarletteHTTPException返回的错误异常,如返回405的异常的时候,走的是这个地方
        app.add_exception_handler(StarletteHTTPException, handler=self.http_exception_handler)
        app.add_exception_handler(RequestValidationError, handler=self.validation_exception_handler)

    async def validation_exception_handler(self, request: Request, exc: RequestValidationError):
        status_code = getattr(exc, "status_code", 400)
        headers = getattr(exc, "headers", None)
        fields = raw_errors_to_fields(exc.raw_errors)

        message = getattr(exc, "message""Validation error.")
        if message and not any(
                [message.endswith("."), message.endswith("?"), message.endswith("!")]
        ):
            message = message + "."  # pragma: no cover

        data = {"message": message, "fields": fields}
        return ParameterException(result=data)

    async def all_exception_handler(self, request: Request, exc: Exception):
        # log_msg = f"捕获到系统错误:请求路径:{request.url.path}\n错误信息:{traceback.format_exc()}"
        if isinstance(exc, StarletteHTTPException) or isinstance(exc, FastapiHTTPException):
            if exc.status_code == 405:
                return MethodnotallowedException()
            if exc.status_code == 404:
                return NotfoundException()
            elif exc.status_code == 429:
                return LimiterResException()
            elif exc.status_code == 500:
                return InternalErrorException()
            elif exc.status_code == 400:
                # 有部分的地方直接的选择使用raise的方式抛出了异常,这里也需要进程处理
                # raise HTTPException(HTTP_400_BAD_REQUEST, 'Invalid token')
                return BadrequestException(msg=exc.detail)
            return BadrequestException()
        else:
            traceback.print_exc()
            return InternalErrorException()

    async def http_exception_handler(self, request: Request, exc: StarletteHTTPException):
        '''
           全局的捕获抛出的HTTPException异常,注意这里需要使用StarletteHTTPException的才可以
           :param request:
           :param exc:
           :return:
           '
''
        if exc.status_code == 405:
            return MethodnotallowedException()
        elif exc.status_code == 404:
            return NotfoundException()
        elif exc.status_code == 429:
            return LimiterResException()
        elif exc.status_code == 500:
            return InternalErrorException()
        else:
            fields = getattr(exc, "fields", [])
            message = getattr(exc, "detail""Validation error.")
            if message and not any(
                    [message.endswith("."), message.endswith("?"), message.endswith("!")]
            ):
                message = message + "."
            data = {
                "message": message,
                "fields": fields,
            }
            return BadrequestException(result=data)


测试使用:

测试代码:

from fastapi import FastAPI
import uvicorn
from response.json_response import Success
from exception import ApiExceptionHandler
app = FastAPI()

@app.on_event("startup")
async def startup_event():
    ApiExceptionHandler().init_app(app)

from typing import List
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field

class Item(BaseModel):
    name: str
    description: str = Field(None, title="标题啊",description="错误提示文字啊", max_length=300)
    price: float = Field(..., gt=0, description="错误提示文字啊")
    tax: float = None



@app.get('/')
async def login(q: str = Query(..., min_length=3,max_length=50),
                q2: str = Query(..., min_length=3,max_length=50),regex="^fixedquery$"):
    return Success()


@app.get('/s')
async def index():
    return Success()


if __name__ == '__main__':
    # 等于通过 uvicorn 命令行 uvicorn 脚本名:app对象 启动服务:
    #  uvicorn xxx:app --reload debug=False 才会去走自己定义的异常信息
    uvicorn.run('main:app',host="127.0.0.1", port=8000, debug=False, reload=True)


测试结果:

总结

以上仅仅是个人结合自己的实际需求,做学习的实践笔记!如有笔误!欢迎批评指正!感谢各位大佬!

结尾

END

简书:https://www.jianshu.com/u/d6960089b087

掘金:https://juejin.cn/user/2963939079225608

公众号:微信搜【小儿来一壶枸杞酒泡茶】

小钟同学 | 文 【欢迎一起学习交流】| QQ:308711822


文章转载自小儿来一壶枸杞酒泡茶,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论