Skip to content

120.修复接口文档打不开的bug

大家好~我是米洛
我正在从0到1打造一个开源的接口测试平台, 也在编写一套与之对应的教程,希望大家多多支持。
欢迎关注我的公众号米洛的测开日记,一起交流学习!

回顾

上一节我们优化了下测试报告的内容。这一节我们来解决一个由来已久的历史bug,顺便弄清楚一下FastApi的启动机制。

本篇内容干货满满,希望大家可以耐心看完,然后给作者一个大大的拥抱。

现象

有同学反馈说本地启动服务的时候,接口文档页面打不开, 其实这个问题我也老早就知道了:

看起来是解析接口文档的时候遇到了问题,下面来说说我的排查思路

1. 找到全局添加的异常

我们找到全局添加的http异常中间件,在里面打印详细的堆栈信息:

app\__init__.py文件

@pity.middleware("http")
async def errors_handling(request: Request, call_next):
    body = await request.body()
    try:
        await set_body(request, await request.body())
        return await call_next(request)
    except Exception as exc:
        import traceback
        # 加上traceback,打印出详细的堆栈信息
        traceback.print_exc()
        return JSONResponse(
            status_code=status.HTTP_200_OK,
            content=jsonable_encoder({
                "code": 110,
                "msg": str(exc),
                "request_data": body,
            })
        )

然后再次请求查看,发现有具体的报错信息了:

提示了我们key error,这看起来不是我们的代码,其实这个是解析你的所有路由里面请求model的方法,我们深入去查看一下:

也就是这一步,key error了,因为他用的是[]而不是.get,也没有任何兜底的操作。那我们看看为啥会KeyError,我们debug一下:

加一个条件断点, 根据key的名字来

当model里面有avatar的时候我们进入断点, 让我们屏气凝神,再试一次:

可以看到,断点进来了。那我们来看看model_name_map里面的keys:

可以看到,里面实际上是有这个数据的?(惊不惊喜意不意外)

然后我们试一下取这个key:

果不其然,还是报错。那我们对比一下2个key,我们看到刚才key的索引是16,我们拉出来对比下:

这2个class,看起来是一丝不差,给我也整懵逼了。


等等,他这个key是class对象,不同的对象做key的话,可能会导致取不到哦。简单的说就是map里面的key != model。

所以我们要考虑下为什么key不等于model,于是我们开始第二步排查。

2. 找找map生成的地方

我们试着找下map生成的地方,看看它的key-value是怎么写进去的!

根据堆栈一路找,可以看到他是根据flat_models解析出的map,我们看看flat_models:

眼疾手快的我们,发现这个flat_models里面居然有2个一毛一样的model,那就可以解释了,key-value肯定是用的后面的那条数据作为key,而我们试着取前面那条数据。

所以真相只有一个:我们这个数据重复了。

3. 是不是接口不小心注册了2次导致?

仔细查看代码,发现并没有2份一样的接口。那会不会是路由重复注册了?那为什么其他的schema没有重复注册呢?

我们来debug一下,这次,我们好好来,看routes部分:

看到route有208个,我们应该没有这么多接口才对呀?这个模式不太好看,我们写个列表推导式see see:

auth接口在前列,我们继续往下找找:

发现这些接口又注册了一遍,好家伙,问题根源找到了,接口注册了2次,所有的都是。

4. 为什么接口注册了2次呢?

回想一下我们启动app的时候,看看控制台:

我们没有开多个workers,但pity is running at pro 被执行了2次,我们找找项目里面的这块代码:

怎么搜也才出现1次,这可尴尬了。于是我们亲手在include_router那里打个断点,看到底会不会执行2次。

第一次include

我们按F9,继续调试代码

发现断点又生效了,下面的日志也打印了2次。证明我们的猜想没错。

5. 求助

这时候拿出各种搜索工具一通搜索,终于发现了这么个地址:

他说他每次都执行3次(好家伙,比我还夸张),我们来看看别人的答复:

简单的说就是,我们执行main.py,在到达__main__代码块之前,include已经执行一次了,接着我们调用的是:

它又会从main.py走一遭,也就是第二次执行。解决方案很简单,我们的启动文件不能放这些include的操作,只需要纯粹地执行代码即可。

import uvicorn

if __name__ == "__main__":
    uvicorn.run("main:pity", host="0.0.0.0", port=7777, reload=False)

所以我这边新建了一个runserver.py,并且写入了以上代码,我们来试试效果:

可以看到,这块内容只打印一次了,我们最后试下打开/docs:

搞定,总体来说花了近2个小时排查+搜索问题,但总归是值得的!