likeadmin和fastapi的bug

以下内容写于2023年8月11日

bug 1

请求体 - 多个参数 - FastAPI (tiangolo.com)中“请求体中的单一值”处,选python3.6,接口示例代码是

from typing import Union

from fastapi import Body, FastAPI
from pydantic import BaseModel
from typing_extensions import Annotated

app = FastAPI()


class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None


class User(BaseModel):
    username: str
    full_name: Union[str, None] = None


@app.put("/items/{item_id}")
async def update_item(
    item_id: int, item: Item, user: User, importance: Annotated[int, Body()]
):
    results = {"item_id": item_id, "item": item, "user": user, "importance": importance}
    return results

根据给的请求体的示例代码,我写的请求代码是

import requests

url = 'http://127.0.0.1:8000/items/7'
form_data = {
    "item": {
        "name": "Foo",
        "description": "The pretender",
        "price": 42.0,
        "tax": 3.2
    },
    "user": {
        "username": "dave",
        "full_name": "Dave Grohl"
    },
    "importance": 5
}
response = requests.put(url=url, json=form_data)
print(response.status_code)
print(response.text)

若新建一个python3.8的环境(装好是3.8.17,称为likeadmin环境),然后按照likeadmin的requirements.txt安装库,运行服务端接口代码,再运行请求代码,不能得到正确的结果,提示

{"detail":[{"loc":["query","importance"],"msg":"field required","type":"value_error.missing"}]}

若新建一个python3.8的环境(装好是3.8.17,称为纯fastapi环境),然后pip install fastapi[all],运行服务端接口代码,再运行请求代码,能得到正确的结果

{"item_id":7,"item":{"name":"Foo","description":"The pretender","price":42.0,"tax":3.2},"user":{"username":"dave","full_name":"Dave Grohl"},"importance":5}

判断结果是否正确的依据是文档中的一句话“在这种情况下,FastAPI 将期望像这样的请求体”。

likeadmin的环境要想得到正确的结果,查看127.0.0.1:8000/docs得知应把importance用问号拼接在请求url中,因为自动生成的文档显示它是query即查询参数。

likeadmin和fastapi的bug_第1张图片 likeadmin的环境自动生成的文档 likeadmin和fastapi的bug_第2张图片 纯fastapi环境自动生成的文档

 总结一下就是likeadmin的环境运行fastapi的示例代码,无法得到fastapi文档中说的预期的结果;而新建一个相同python版本的虚拟环境,里面只pip install fastapi[all],可以得到fastapi文档中说的预期的结果。

截止到2023年8月11日,likeadmin的requirements.txt的内容是

aiofiles==22.1.0
aiomysql==0.1.1
alibabacloud_dysmsapi20170525==2.0.23
alibabacloud_tea_openapi
anyio==3.6.1
async-timeout==4.0.2
click==8.1.3
colorama==0.4.5
commonmark==0.9.1
cos-python-sdk-v5==1.9.22
databases==0.6.1
Deprecated==1.2.13
fastapi==0.85.0
fastapi-cache2==0.1.9
fastapi-pagination==0.10.0
greenlet==1.1.3.post0
h11==0.14.0
httptools==0.5.0
idna==3.4
Jinja2==3.1.2
MarkupSafe==2.1.1
optionaldict==0.1.2
oss2==2.16.0
pendulum==2.1.2
psutil==5.9.3
pydantic==1.10.2
Pygments==2.13.0
PyMySQL==1.0.2
python-dateutil==2.8.2
python-dotenv==0.21.0
python-multipart==0.0.5
pytz==2022.5
pytzdata==2020.1
PyYAML==6.0
qiniu==7.9.0
redis==4.4.0rc2
rich==12.6.0
shellingham==1.5.0
six==1.16.0
sniffio==1.3.0
SQLAlchemy==1.4.41
starlette==0.20.4
tencentcloud-sdk-python==3.0.841
typer==0.6.1
typing-extensions==4.4.0
ua-parser==0.16.1
user-agents==2.2.0
# uvloop==0.17.0
uvicorn==0.18.3
watchfiles==0.17.0
websockets==10.3
wrapt==1.14.1
wechatpy==1.8.18

bug 2

与bug1类似,也是likeadmin的环境运行fastapi的示例代码,无法得到fastapi文档中说的预期的结果;而新建一个相同python版本的虚拟环境,里面只pip install fastapi[all],可以得到fastapi文档中说的预期的结果。

请求体 - 多个参数 - FastAPI (tiangolo.com)中“嵌入单个请求体参数”处,选python3.6,接口示例代码是

from typing import Union

from fastapi import Body, FastAPI
from pydantic import BaseModel
from typing_extensions import Annotated

app = FastAPI()


class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]):
    results = {"item_id": item_id, "item": item}
    return results

文档中说“在这种情况下,FastAPI 将期望像这样的请求体:”

{
    "item": {
        "name": "Foo",
        "description": "The pretender",
        "price": 42.0,
        "tax": 3.2
    }
}

而不是:

{
    "name": "Foo",
    "description": "The pretender",
    "price": 42.0,
    "tax": 3.2
}

根据给的请求体的示例代码,我写的请求代码是

import requests

url = 'http://127.0.0.1:8000/items/7'
form_data_1 = {
    "item": {
        "name": "Foo",
        "description": "The pretender",
        "price": 42.0,
        "tax": 3.2
    }
}
form_data_2 = {
    # "item": {
        "name": "Foo",
        "description": "The pretender",
        "price": 42.0,
        "tax": 3.2
    # }
}
response_1 = requests.put(url=url, json=form_data_1)
response_2 = requests.put(url=url, json=form_data_2)
print(response_1.status_code)
print(response_1.text)
print(response_2.status_code)
print(response_2.text)
# 上面的打印结果:
# fastapi环境
# 200
# {"item_id":7,"item":{"name":"Foo","description":"The pretender","price":42.0,"tax":3.2}}
# 422
# {"detail":[{"type":"missing","loc":["body","item"],"msg":"Field required","input":null,"url":"https://errors.pydantic.dev/2.1/v/missing"}]}
# likeadmin环境
# 422
# {"detail":[{"loc":["body","name"],"msg":"field required","type":"value_error.missing"},{"loc":["body","price"],"msg":"field required","type":"value_error.missing"}]}
# 200
# {"item_id":7,"item":{"name":"Foo","description":"The pretender","price":42.0,"tax":3.2}}

bug 3

Header 参数 - FastAPI (tiangolo.com)中"声明 Header 参数"处(还有Cookie 参数 - FastAPI (tiangolo.com)中"声明 Cookie 参数"处),选python3.6,接口示例代码是

from typing import Union

from fastapi import FastAPI, Header
from typing_extensions import Annotated

app = FastAPI()


@app.get("/items/")
async def read_items(user_agent: Annotated[Union[str, None], Header()] = None):
    return {"User-Agent": user_agent}
from typing import Union

from fastapi import Cookie, FastAPI
from typing_extensions import Annotated

app = FastAPI()


@app.get("/items/")
async def read_items(ads_id: Annotated[Union[str, None], Cookie()] = None):
    return {"ads_id": ads_id}

 我实际写的接口代码是

from typing import Union

from fastapi import FastAPI, Header, Cookie
from typing_extensions import Annotated

app = FastAPI()

@app.get("/items/")
async def read_items(
    BIDUPSID: Annotated[Union[str, None], Cookie()] = None,  # 从请求头的cookie中获取键为BIDUPSID的值
    PSTM: Annotated[Union[str, None], Cookie()] = None,
    BA_HECTOR: Annotated[Union[str, None], Cookie()] = None,
    user_agent: Annotated[Union[str, None], Header()] = None  # fastapi自动将user_agent转为user-agent,又因HTTP headers是大小写不敏感的,所以user_agent就是User-Agent。from https://fastapi.tiangolo.com/zh/tutorial/header-params/#_1中"自动转换处"
    ):
    return {"Cookie": f'BIDUPSID={BIDUPSID}; PSTM={PSTM}; BA_HECTOR={BA_HECTOR}', "User-Agent": user_agent}

'''
# from https://fastapi.tiangolo.com/zh/tutorial/header-params/#_1中"自动转换处"
自动转换
Header 在 Path, Query 和 Cookie 提供的功能之上有一点额外的功能。

大多数标准的headers用 "连字符" 分隔,也称为 "减号" (-)。

但是像 user-agent 这样的变量在Python中是无效的。

因此, 默认情况下, Header 将把参数名称的字符从下划线 (_) 转换为连字符 (-) 来提取并记录 headers.

同时,HTTP headers 是大小写不敏感的,因此,因此可以使用标准Python样式(也称为 "snake_case")声明它们。

因此,您可以像通常在Python代码中那样使用 user_agent ,而不需要将首字母大写为 User_Agent 或类似的东西。

如果出于某些原因,你需要禁用下划线到连字符的自动转换,设置Header的参数 convert_underscores 为 False
'''

我写的请求代码是

import requests

url = 'http://127.0.0.1:8000/items/'
headers = {
    'Cookie': 'BIDUPSID=A9DA10BEC1294DD8D4F6E654A52CBDC8; PSTM=1691732828; BAIDUID=A9DA10BEC1294DD8402C67E5E8156634:FG=1; BD_UPN=12314753; BA_HECTOR=8h8k05052l000la0a1810h0j1idbjgf1p; BAIDUID_BFESS=A9DA10BEC1294DD8402C67E5E8156634:FG=1; BD_CK_SAM=1; PSINO=2; ZFY=FJzChFI0tlXn0pdMbu9GDJzHS1iNKW8:A:AjdKkCxk2W0:C; delPer=0; BD_HOME=1; COOKIE_SESSION=21_0_0_0_3_0_0_0_0_0_0_0_23_0_4_0_1691734574_0_1691734570%7C2%230_0_1691734570%7C1; H_PS_PSSID=36546_39112_38831_39008_39114_39038_38917_26350_39138_39132_39137_22157_39100; H_PS_645EC=d157JRTS54%2FnnbXzJd5jNKP%2FMviUleulBfUEwB4MIKKEUrwiHAKJcjpnt4A',
    'Referer': 'https://www.baidu.com/',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.203'
}
# cookie = {
#     'BIDUPSID': 'A9DA10BEC1294DD8D4F6E654A52CBDC8',
#     'PSTM': '1691732828',
#     'BA_HECTOR': '8h8k05052l000la0a1810h0j1idbjgf1p'
# }

response = requests.get(url=url, headers=headers)
# response = requests.get(url=url, headers=headers, cookies=cookie)  # 注释掉headers中Cookie,取消cookie的注释,再用这个请求,和上一行的结果一样
print(response.status_code)
print(response.text)

 likeadmin的环境获取不到Cookie和User-Agent;纯fastapi环境,Cookie和User-Agent都能获取。 

# likeadmin的环境的结果
{"Cookie":"BIDUPSID=None; PSTM=None; BA_HECTOR=None","User-Agent":null}

# 纯fastapi环境的结果
{"Cookie":"BIDUPSID=A9DA10BEC1294DD8D4F6E654A52CBDC8; PSTM=1691732828; BA_HECTOR=8h8k05052l000la0a1810h0j1idbjgf1p","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.203"}

bug 4

路径操作配置 - FastAPI (tiangolo.com)中"status_code状态码"处,接口示例代码是

from typing import Set, Union

from fastapi import FastAPI, status
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: Set[str] = set()


@app.post("/items/", response_model=Item, status_code=status.HTTP_201_CREATED)  # 也可以直接用status_code=201
async def create_item(item: Item):
    return item

我写的请求代码是

import requests

url = 'http://127.0.0.1:8000/items/'
form_data = {
    'name': 'Bob',
    'price': 12.3,
}
response = requests.post(url=url, json=form_data)
print(response.status_code)
print(response.text)

 likeadmin的环境打印的状态码还是200,没变成设置的201;纯fastapi环境的是201。

bug 1 与 bug 2 与 bug 3 与 bug 4的解决

既然两个虚拟环境的python版本一样,忽然想到是不是fastapi版本不一样导致的,查看纯fastapi环境(2023年8月11日)得到

Package              Version
-------------------- -----------
annotated-types      0.5.0
anyio                3.7.1
certifi              2023.7.22
click                8.1.6
colorama             0.4.6
dnspython            2.4.2
email-validator      2.0.0.post2
exceptiongroup       1.1.2
fastapi              0.101.0
h11                  0.14.0
httpcore             0.17.3
httptools            0.6.0
httpx                0.24.1
idna                 3.4
itsdangerous         2.1.2
Jinja2               3.1.2
MarkupSafe           2.1.3
orjson               3.9.4
pip                  23.2.1
pydantic             2.1.1
pydantic_core        2.4.0
pydantic-extra-types 2.0.0
pydantic-settings    2.0.2
python-dotenv        1.0.0
python-multipart     0.0.6
PyYAML               6.0.1
setuptools           68.0.0
sniffio              1.3.0
starlette            0.27.0
typing_extensions    4.7.1
ujson                5.8.0
uvicorn              0.23.2
watchfiles           0.19.0
websockets           11.0.3
wheel                0.38.4

将likeadmin的fastapi库由0.85.0升级到和纯fastapi环境一样的0.101.0,再运行上面的示例代码,就能得到正确的结果了。

你可能感兴趣的:(fastapi,python)