大家好~我是
米洛
!
我正在从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,
})
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
然后再次请求查看,发现有具体的报错信息了:
提示了我们key error,这看起来不是我们的代码,其实这个是解析你的所有路由里面请求model的方法,我们深入去查看一下:
也就是这一步,key error了,因为他用的是[]而不是.get,也没有任何兜底
的操作。那我们看看为啥会KeyError,我们debug一下:
当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次。
我们按F9,继续调试代码
发现断点又生效了,下面的日志也打印了2次。证明我们的猜想没错。
# 5. 求助
这时候拿出各种搜索工具一通搜索,终于发现了这么个地址 (opens new window):
他说他每次都执行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)
2
3
4
所以我这边新建了一个runserver.py,并且写入了以上代码,我们来试试效果:
可以看到,这块内容只打印一次了,我们最后试下打开/docs:
搞定,总体来说花了近2个小时排查+搜索问题,但总归是值得的!