数据工厂系列(5)新增自定义异常处理器

411 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第5天,点击查看活动详情

大家好~我是小方,欢迎大家关注笋货测试笔记体完记得俾个like

接上篇

上篇我们只是优化了部分代码,剩下的优化点,我们今天全都搞定~

新增自定义异常处理器

  • 自定义http异常处理器 注意看上图,我们的注册接口是post请求,但是我直接用了get请求,fastapi自带返回了错误新,但是不满足我们的统一返参结构,所以要加上一个自定义http异常处理器处理该情况~app/init.py加入以下代码
@fun.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
    res = ResponseDto(code=201, msg=HTTP_CODE_MSG.get(exc.status_code, exc.detail))
    return JSONResponse(content=res.dict())

另外,我在config.py文件加了下面http状态码的map,不多,有兴趣的小伙伴可行添加~ 测试一下~!

  • 自定义参数校验异常处理器

有时候请求的入参跟我们定义的模型入参不一致时,fastapi默认会返回一个detail信息,告诉你具体哪里错了,但是默认的返参结构不满足们的统一返参结构,得改! app/init.py加入以下代码

@fun.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    message = ""
    for error in exc.errors():
        message += str(error.get('loc')[-1]) + ":" + str(error.get("msg"))+","
    res = ResponseDto(code=201, msg=f"请求参数非法! {message[:-1]}")
    return JSONResponse(content=res.dict())

测试一下~

  • 手动捕获异常处理器

上篇我们讲到虽然成功捕获了异常,但是返参并不是我们想要的结构,改! app/init.py加入以下代码

@fun.exception_handler(NormalException)
async def unexpected_exception_error(request: Request, exc: NormalException):
    res = ResponseDto(code=110, msg=exc.detail)
    return JSONResponse(content=res.dict())

测试一下,重复邮箱号进行注册

  • 捕获异常进阶

上面我们加了手动捕获异常处理器,但是如果我们想要添加日志,记录报错的请求参数该怎么做呢?请看下面~加上这段代码即可

重新请求一下,发现接口出现堵塞了

大概原因是request请求被消耗掉了,导致异步程序卡死在await request.json()???最后参考无敌哥的pity,成功解决~

历史踩坑记,请看:testerhome.com/topics/2890…

完整代码

# -*- coding: utf-8 -*- 
# @Time : 2022/5/3 08:56 
# @Author : junjie
# @File : __init__.py

import json
from fastapi import FastAPI, Request
from config import Text, HTTP_CODE_MSG
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.types import Message
from fastapi.exceptions import RequestValidationError
from app.models.base import ResponseDto
from fastapi.responses import JSONResponse
from app.utils.exception_utils import NormalException
from app.utils.logger import Log



fun = FastAPI(title=Text.TITLE, version=Text.VERSION, description=Text.DESCRIPTION)

async def set_body(request: Request, body: bytes):
    async def receive() -> Message:
        return {"type": "http.request", "body": body}
    request._receive = receive


async def get_body(request: Request) -> bytes:
    body = await request.body()
    await set_body(request, body)
    return body

# 自定义http异常处理器
@fun.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
    res = ResponseDto(code=201, msg=HTTP_CODE_MSG.get(exc.status_code, exc.detail))
    return JSONResponse(content=res.dict())

# 自定义参数校验异常处理器
@fun.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    message = ""
    for error in exc.errors():
        message += str(error.get('loc')[-1]) + ":" + str(error.get("msg"))+","
    res = ResponseDto(code=101, msg=f"请求参数非法! {message[:-1]}")
    return JSONResponse(content=res.dict())

# 手动捕获异常处理器
@fun.exception_handler(NormalException)
async def unexpected_exception_error(request: Request, exc: NormalException):
    log_msg = f"数据工厂捕获到异常, 请求路径: {request.url.path}\n"
    params = request.query_params
    headers = request.headers
    if params: log_msg += f"路径参数: {params}\n"
    if headers.get('content-type') == 'application/json':
        body = json.dumps(await request.json(), ensure_ascii=False)
        log_msg += f"请求参数: {body}"
    Log().info(log_msg)
    res = ResponseDto(code=110, msg=exc.detail)
    return JSONResponse(content=res.dict())


# 全局捕获异常
@fun.middleware("http")
async def errors_handling(request: Request, call_next):
    log_msg = f"数据工厂捕获到系统错误, 请求路径:{request.url.path}\n"
    params = request.query_params
    body = await request.body()
    headers = request.headers
    if params:
        log_msg += f"路径参数: {params}\n"
    if body and headers.get('content-type') == 'application/json':
        try:
            body = json.dumps(json.loads(body), ensure_ascii=False)
            log_msg += f"请求参数: {body}\n"
        except:
            res = ResponseDto(code=102, msg='解析json失败')
            return JSONResponse(content=res.dict())
    try:
        await set_body(request, await request.body())
        return await call_next(request)
    except Exception as exc:
        import traceback
        log_msg += f"错误信息: {traceback.format_exc()}"
        Log().error(log_msg)
        res = ResponseDto(code=500, msg=str(exc.args[0]))
        return JSONResponse(content=res.dict())

最终正常打印日志

注册路由优化

新建app/router/routers.py(或者将底下的init.py改一下名即可)加入以下代码

from app.routers.user import user


data = [
    (user.router, '/api/user', ["用户模块"])
]

app/init.py引入

main.py相关的代码也清空掉

重启服务,打开http://127.0.0.1:8080/docs#/

初始化建表优化

import os, importlib, sys
from app.models import Base, engine
from config import FilePath


def get_curd_path():
    """获取curd目录下所有的xxxDao.py"""
    curd_path_list = []
    for file in os.listdir(FilePath.CURD_PATH):
        # 拼接目录
        file_path = os.path.join(FilePath.CURD_PATH, file)
        # 判断过滤, 取有效目录
        if os.path.isdir(file_path) and '__pycache__' not in file:
            from collections import defaultdict
            path_dict = defaultdict(list)
            # 获取目录下所有的xxxDao.py
            for py_file in os.listdir(file_path):
                if py_file.endswith('py') and 'init' not in py_file:
                    path_dict[file].append(py_file.split('.')[0])
            curd_path_list.append(path_dict)
    return curd_path_list

dao_path_list = get_curd_path()
for path in dao_path_list:
    for file_path,pys in path.items():
        # 拼接对应的curd目录
        son_dao_path = os.path.join(FilePath.CURD_PATH, file_path)
        # 导包时, 默认在这个路径下查找
        sys.path.append(son_dao_path)
        for py in pys:
            # 动态导包进去
            importlib.import_module(py)

Base.metadata.create_all(engine)

config.py加入这两个路径

其实原理就是动态导包进去,将curd目录下的xxxDao导入进去,然后进行Base.metadata.create_all(engine)就可以批量创建表了 删掉表测试一下

重启服务后,刷新表

搞定~

总结

结合本篇和上篇,我们已经将所有的优化点处理完毕:

  • 抛异常时,没有处理,直接服务器错误了 [✓]
  • schema校验不通过时,返参太难看了[✓]
  • 初始化建表时,需要在main.py文件引入,不太方便[✓]
  • 返参没有统一的格式 [✓]
  • 编写UserDao时,需要手动try...except```比较繁琐... [✓]
  • user表的密码明文存储,不太安全 [✓]
  • 注册router时,需要一个个include_router,太麻烦了[✓]
  • ...

好了,今天先到这里了,我们下期见哈~