fastapi 实践

Uvicorn 启动

启动实例

import uvicorn
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def index():
	return {"message": "Hello World"}
if __name__ == "__main__":
	# uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True)
	# 0.0.0.0 支持IP访问
	uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

现在将这个 app.py 作为 Python 脚本运行,如下所示:

(fastapienv) user@ubuntu:~/work$ python app.py

参数说明

--host TEXT:# 将套接字绑定到这个主机。默认为127.0.0.1 。
--port INTEGER:# 将套接字绑定到这个端口。默认为8000。
--uds TEXT:# 绑定到一个UNIX域套接字。
-- fd INTEGER:# 从这个文件描述符绑定到套接字。
--reload:# 启用自动重载。
--reload-dir PATH# 明确设置重载目录,默认为当前工作目录。
--reload-include TEXT:# 观察时包括文件。默认包括’*.py’。
--reload-exclude TEXT:# 观察文件时排除。
--reload-delay FLOAT:# 上一次和下一次检查之间的延迟,默认为0.25
--loop [auto|asyncio|uvloop]# 事件循环的实现。[默认为自动]。
--http [auto|h11|httptools]# HTTP协议实现。[默认为自动]。
--interface auto|asgi|asgi|wsgi:# 选择应用程序接口。[默认为自动]。
--env-file PATH# 环境配置文件。
--log-config PATH# 日志配置文件。支持的格式是.ini, .json, .yaml。
--version:# 显示uvicorn的版本并退出。
--app-dir TEXT:# 在指定的目录中查找APP,默认为当前目录
--help:# 显示此信息并退出。

FastAPI 参数

路径参数

import uvicorn
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def index():
   return {"message": "Hello World"}
@app.get("/hello/{name}")
async def hello(name):
   return {"name": name}

启动 Uvicorn 服务器并访问 http://localhost:8000/hello/Tutorialspoint URL。浏览器显示以下JSON响应。

{"name":"Tutorialspoint"}

多个路径参数

from fastapi import FastAPI
app = FastAPI()
@app.get("/hello/{name}/{age}")
async def hello(name,age):
   return {"name": name, "age":age}

在这种情况下, /hello 是路线,后面是两个放在大括号里的参数。如果浏览器地址栏中给出的 URL 是 http://localhost:8000/hello/Ravi/20,Ravi和20的数据将被分别分配给变量name和age。浏览器会显示以下的JSON响应

{"name":"Ravi","age":"20"}

带类型的路径参数
你可以对要装饰的函数参数使用 Python 的类型提示。在这种情况下,将 name 定义为 str,age 定义为 int。

@app.get("/hello/{name}/{age}")
async def hello(name:str,age:int):
   return {"name": name, "age":age} 

如果类型不匹配,这将导致浏览器在JSON响应中显示一个HTTP错误信息。尝试输入http://localhost:8000/hello/20/Ravi 作为URL。浏览器的响应将如下 –

{
   "detail": [
      {
         "loc": [
            "path",
            "age"
         ],
      "msg": "value is not a valid integer",
      "type": "type_error.integer"
      }
   ]
}

查询参数

向服务器传递请求数据的经典方法是在URL上附加一个查询字符串。例如 –
http://localhost/cgi-bin/hello.py?name=Ravi&age=20,它以问号(?)作为分隔符被附加到URL上,一个由and(&)连接的键值对列表构成了查询字符串。URL的尾部,即(?)之后的部分是查询字符串,然后由服务器端脚本进行解析,以便进一步处理。

from fastapi import FastAPI
app = FastAPI()
@app.get("/hello")
async def hello(name:str,age:int):
   return {"name": name, "age":age}

在浏览器中启动Uvicorn服务器和这个URL:http://localhost:8000/hello?name=Ravi&age=20
curl -i ‘http://192.168.98.174:8000/hello?name=nihao&age=10’

{"name":"nihao","age":10}

参数验证

可以对路径参数以及URL的查询参数应用 验证条件 。为了在路径参数上应用验证条件,你需要导入路径类。除了参数的默认值外,如果是字符串参数,你可以指定最大和最小长度。

from fastapi import FastAPI, Path
app = FastAPI()
@app.get("/hello/{name}")
async def hello(name:str=Path(...,min_length=3,max_length=10)):
   return {"name": name}

如果浏览器的URL包含长度小于3或大于10的参数,如(http://localhost:8000/hello/Tutorialspoint),则会出现适当的错误消息。

    # 查询参数
    # curl -i 'http://192.168.98.174:8000/more/items/?skip=0&limit=10'
    # 加上 alias 之后 url包含skip的 被弃用
    # curl -i 'http://192.168.98.174:8000/more/items/?index=0&limit=10'
    # Query 有多个参数:
    #   description: 参数描述
    #   max_length 和 min_length: 参数最大最小长度
    #   regex:正则表达式
    #   alias:参数别名
    #   title:参数标题
    #   deprecated:弃用
    # Path 有多个参数, 以下参数 Query 也有。区分没有详细测试
    #   gt:大于(greater than)
    #   ge:大于等于(greater than or equal)
    #   lt:小于(less than)
    #   le:小于等于(less than or equal)
    @app.get("/more/items/")
    async def read_more(skip: int = Query(alias = "index", lt=10), limit: Optional[int] = 11):
        try:
            data={"skip": skip, "limit": limit}
            return respSuccessJson(data=data, message='获取成功')
        except Exception as e:
            logger.error(f"error: {e}")
            return respErrorJson(err=ERROR_SERVICE_ERROR, data=str(e))

Pydantic

参考往期文章
https://blog.csdn.net/cliffordl/article/details/134070532

上传文件

参考:
https://geek-docs.com/fastapi/fastapi-tutorials/t_lib_fastapi_17_fastapi_uploading_files.html

下载文件

header参数

为了读取作为客户端请求一部分的 HTTP头的 值,从FastAPI库中导入Header对象,并在操作函数定义中声明一个Header类型的参数。参数的名称应该与用 camel_case 转换的HTTP头相匹配 。
在下面的例子中,要检索的是 “accept-language “头。由于Python不允许在标识符的名称中使用”-“(破折号),所以用”_”(下划线)代替。

from typing import Optional
from fastapi import FastAPI, Header
app = FastAPI()
@app.get("/headers/")
async def read_header(accept_language: Optional[str] = Header(None)):
   return {"Accept-Language": accept_language} 

你可以在响应对象中推送自定义和预定义的头信息。 操作函数应该有一个响应类型的参数。为了设置一个自定义的头信息,它的名字应该以 “ X “ 为前缀 。 在下面的例子中,一个名为 “X-Web-Framework “的自定义头和一个预定义头 “Content-Language “被添加到操作函数的响应中。

from fastapi import FastAPI
from fastapi.responses import JSONResponse
app = FastAPI()
@app.get("/rspheader/")
def set_rsp_headers():
   content = {"message": "Hello World"}
   headers = {"X-Web-Framework": "FastAPI", "Content-Language": "en-US"}
   return JSONResponse(content=content, headers=headers)

实例1 基础

# pip install pydantic
# pip install fastapi
## pip install uvicorn

import argparse
import sys
import logging
import uvicorn
from fastapi import FastAPI, Request, Query, status, Header, Depends, HTTPException
from fastapi.responses import JSONResponse, StreamingResponse
from fastapi.security import OAuth2PasswordBearer
from typing import Optional, Union, List
from pydantic import BaseModel

import uuid
#from utils.configs import load_config, Config
#from utils.dbmanage import DBManage
##from utils.task import TaskBase, TaskInDB, QuestionList, QuestionInDB, g_task_dict, ScoreList, BattelList
#from utils.task import TaskBase, TaskInDB, QuestionList, g_task_dict
#from utils.tokenmanage import TokenBase, checkout_token
#
#from utils.tokenizer import train_tokenizer, count_tokens
##from utils.streaming import _query

import asyncio
from asyncio import Queue


from jose import JWTError, jwt
# 创建密钥变量
# to get a string like this run:
# openssl rand -hex 32
# 生成一个随机的密钥,用于对JWT令牌进行签名
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
# 创建用于设定JWT令牌签名算法的变量
ALGORITHM = "HS256"
# 创建设置令牌过期时间变量(单位:分钟)
ACCESS_TOKEN_EXPIRE_MINUTES = 30


def get_logger(name, file_name, use_formatter=True):
    logger = logging.getLogger(name)
    logger.setLevel(logging.INFO)
    console_handler = logging.StreamHandler(sys.stdout)
    formatter = logging.Formatter('%(asctime)s    %(message)s')
    console_handler.setFormatter(formatter)
    console_handler.setLevel(logging.INFO)
    logger.addHandler(console_handler)
    if file_name:
        handler = logging.FileHandler(file_name, encoding='utf8')
        handler.setLevel(logging.INFO)
        if use_formatter:
            formatter = logging.Formatter('%(asctime)s %(name)s: %(message)s')
            handler.setFormatter(formatter)
        logger.addHandler(handler)
    return logger

#app = FastAPI()

def start_server(host: str, port: int):
    print(f"start_server host:{host} port:{port}")
    class ErrorBase(BaseModel):
        code: int
        message: str = ""
    
    # 找不到路径
    ERROR_NOT_FOUND                 = ErrorBase(code=404, message="api 路径错误")
    # 参数错误
    ERROR_PARAMETER_ERROR           = ErrorBase(code=400, message="参数错误")
    # 用户相关
    ERROR_SERVICE_ERROR             = ErrorBase(code=5001, message="内部错误")
    ERROR_USER_TOKEN_FAILURE        = ErrorBase(code=5004, message="未登录或登录过期")
    ERROR_USER_NOT_FOUND            = ErrorBase(code=5004, message="用户不存在")
    ERROR_USER_PASSWORD_ERROR       = ErrorBase(code=5005, message="密码错误")
    ERROR_USER_NOT_ACTIVATE         = ErrorBase(code=5006, message="用户账号尚未")
    ERROR_USER_ACCOUNT_EXISTS       = ErrorBase(code=5007, message="账号已存在")
    ERROR_USER_EMAIL_NOT_EXISTS     = ErrorBase(code=5008, message="邮箱不存在")
    ERROR_FORGET_PWD_TOKEN_ERROR    = ErrorBase(code=5009, message="重置密码链接错误或已过期")
    ERROR_USER_REGISTER_TOKEN_ERROR = ErrorBase(code=5031, message="注册验证链接已过期或不存在")
    ERROR_USER_REGISTER_EXISTS      = ErrorBase(code=5032, message="注册失败,可能账号已存在。")
    ERROR_USER_REGISTER_ERROR       = ErrorBase(code=5033, message="注册失败,请重试。")
    ERROR_USER_REGISTER_TO_OFTEN    = ErrorBase(code=5034, message="提交注册太频繁,请稍后重试")
    ERROR_USER_EMAIL_EXISTS         = ErrorBase(code=5011, message="邮箱不可用")
    ERROR_USER_PHONE_EXISTS         = ErrorBase(code=5012, message="手机号码不可用")
    ERROR_USER_USERNAME_EXISTS      = ErrorBase(code=5013, message="用户名不可用")
    ERROR_USER_CAPTCHA_CODE_ERROR   = ErrorBase(code=5021, message="验证码错误")
    ERROR_USER_CAPTCHA_CODE_INVALID = ErrorBase(code=5022, message="验证码已失效,请重试。")
    ERROR_USER_PREM_ADD_ERROR       = ErrorBase(code=5031, message="权限标识添加失败")
    ERROR_USER_PREM_ERROR           = ErrorBase(code=5403, message="权限不足")

    """ 接口成功返回 """
    def respSuccessJson(data: Union[list, dict, str] = None, message: str = "Success"):
        return JSONResponse(
            status_code=status.HTTP_200_OK,
            content={
                'code': 0,
                'messgae': message,
                'data': data or {}
            })
    """ 错误接口返回 """
    def respErrorJson(err: ErrorBase, *, message: Optional[str] = None, msg_append: str = "",
                  data: Union[list, dict, str] = None, status_code: int = status.HTTP_200_OK):
        return JSONResponse(
            status_code=status_code,
            content={
                'code': err.code,
                'message': (message or err.message) + msg_append,
                'data': data or {}
            })

    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )

    app = FastAPI()

    async def token_is_true(token: str = Header(..., description="token验证")):
        print(token)
        return token

    # 定义token创建和获取方法
    #oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/login")
    oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


    class TokenBase(BaseModel):
        token: str
        user_id: str


    # 定义解密token的方法
    def fake_decode_token(token):
        try:
            print("fake_decode_token", token)
            payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
            username: str = payload.get("sub")
            print("fake_decode_token", username)
            if username is None:
                return False
            token_base = TokenBase(token=token, user_id=username)
            return token_base
        except Exception as e:
            return False

    # 定义获取当前用户的方法
    # 依赖oauth2_scheme去获取请求头中是否有Authorization,其对应的值应该是token
    def get_current_user(token:str = Depends(oauth2_scheme)):
        # 解析token
        token_base = fake_decode_token(token)
        print("get_current_user", token_base)
        # 用户不存在时报错
        if not token_base:
            raise credentials_exception
            #return False
        print("get_current_user", token_base)
        return token_base

    # curl -i 'http://192.168.98.174:8008/'
    # 返回:{"code":0,"messgae":"获取成功","data":"welcome to my service"}
    @app.get("/")
    async def index():
        try:
            data="welcome to my service"
            return respSuccessJson(data=data, message='获取成功')
        except Exception as e:
            return respErrorJson(err=ERROR_SERVICE_ERROR, data=str(e))
 
    # curl -i 'http://192.168.98.174:8008/items/122'
    # 返回:{"code":0,"messgae":"获取成功","data":{"item_id":"122"}}
    @app.get("/items/{item_id}")
    async def read_item(item_id):
        try:
            data={"item_id": item_id}
            return respSuccessJson(data=data, message='获取成功')
        except Exception as e:
            return respErrorJson(err=ERROR_SERVICE_ERROR, data=str(e))

    # 查询参数
    # curl -i 'http://192.168.98.174:8008/more/items/?skip=0&limit=10'
    # 加上 alias 之后 url包含skip的 被弃用
    # curl -i 'http://192.168.98.174:8008/more/items/?index=0&limit=10'
    # 返回:{"code":0,"messgae":"获取成功","data":{"skip":0,"limit":10}}
    # Query 有多个参数:
    #   description: 参数描述
    #   max_length 和 min_length: 参数最大最小长度
    #   regex:正则表达式
    #   alias:参数别名
    #   title:参数标题
    #   deprecated:弃用
    # Path 有多个参数, 以下参数 Query 也有。区分没有详细测试
    #   gt:大于(greater than)
    #   ge:大于等于(greater than or equal)
    #   lt:小于(less than)
    #   le:小于等于(less than or equal)
    @app.get("/more/items/")
    async def read_more(skip: int = Query(alias = "index", lt=10), limit: Optional[int] = 11):
        try:
            data={"skip": skip, "limit": limit}
            return respSuccessJson(data=data, message='获取成功')
        except Exception as e:
            logger.error(f"error: {e}")
            return respErrorJson(err=ERROR_SERVICE_ERROR, data=str(e))

    # 查询参数,列表
    @app.get("/more/list/")
    ## curl -i 'http://192.168.98.174:8008/more/list/?name=tom&name=lily'
    ## 运行结果:{"code":0,"messgae":"获取成功","data":{"name":["tom","lily"]}}
    #async def read_items(name: Union[List[str], None] = Query(default=None)):
    # curl -i 'http://192.168.98.174:8008/more/list/'
    # 运行结果:{"code":0,"messgae":"获取成功","data":{"name":["foo","bar"]}}
    async def read_items(name: Union[List[str], None] = Query(default=["foo", "bar"])):
        try:
            data={"name": name}
            return respSuccessJson(data=data, message='获取成功')
        except Exception as e:
            logger.error(f"error: {e}")
            return respErrorJson(err=ERROR_SERVICE_ERROR, data=str(e))

    class User(BaseModel):
        name: str
        age: Optional[int] = 0

    class UserInDB(User):
        user_id: str

    # curl -X 'POST'  'http://192.168.98.174:8008/pydantic/' -H 'Content-Type: application/json' -d '{"name":"tom"}'
    # 运行结果:{"code":0,"messgae":"获取成功","data":{"name":"tom","age":0}}
    @app.post("/pydantic/")
    async def pydantic(user: User):
        try:
            data=user.dict()
            return respSuccessJson(data=data, message='获取成功')
        except Exception as e:
            logger.error(f"error: {e}")
            return respErrorJson(err=ERROR_SERVICE_ERROR, data=str(e))
    
    # curl -X 'POST'   'http://192.168.98.174:8008/task/create'  -H 'Content-Type: application/json'  -H 'accept: application/json'    -H 'User-Agent: FastChat Client'  -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJoZWxsbyIsImV4cCI6MTcxMDg2MjIxMX0.Lcq42LitPlXViRNPOhfpwG6koh_yml7RtrvbPvIWnYM'   -d '{"name":"tom"}'
    # 返回:{"code":0,"messgae":"创建成功","data":{"name":"tom","age":0,"user_id":"fddb0d0f4021478e98f81c1e0a372d9d"}}
    @app.post('/task/create')
    # 以下调用 只返回token字符串
    #async def task_create(task_base: TaskBase, token: str = Depends(oauth2_scheme)):
    # 以下调用返回 用户信息
    async def task_create(user: User, token_base: TokenBase=Depends(get_current_user)):
        try:
            if not token_base:
                logger.error(f"token error")
                return respErrorJson(err=ERROR_USER_TOKEN_FAILURE, data="Could not validate credentials")
            print("task_create", user)
            
            user_id = str(uuid.uuid4().hex)
            print("task_create", uuid)
            user_info = UserInDB(**user.dict(), user_id=user_id)

            return respSuccessJson(data=user_info.dict(), message='创建成功')
        except Exception as e:
            return respErrorJson(err=ERROR_SERVICE_ERROR, data=str(e))
    
    # 文件路径    
    # curl -i http://192.168.98.174:8008/files//home/johndoe/myfile.txt
    # 运行结果:{"code":0,"messgae":"获取成功","data":{"file_path":"/home/johndoe/myfile.txt"}}
    @app.get("/files/{file_path:path}")
    async def read_file(file_path: str):
        try:
            data = {"file_path": file_path}
            return respSuccessJson(data=data, message='获取成功')
        except Exception as e:
            logger.error(f"error: {e}")
            return respErrorJson(err=ERROR_SERVICE_ERROR, data=str(e))

    # curl -i 'http://192.168.98.174:8008/request/'
    # 运行结果:{"headers":{"host":"192.168.98.174:8008","user-agent":"curl/7.87.0","accept":"*/*"}}
    #@app.get("/request/")
    # curl -X 'POST' 'http://192.168.98.174:8008/request/' -H 'Content-Type: application/json'
    # 运行结果:{"headers":{"host":"192.168.98.174:8008","user-agent":"curl/7.87.0","accept":"*/*","content-type":"application/json","content-length":"14"}}
    @app.post("/request/")
    async def read_select(request: Request):
        try:
            return {"headers": request.headers}
            # request.headers is not json
            #data={"headers": request.headers["host"]}
            return respSuccessJson(data=data, message='获取成功')
        except Exception as e:
            logger.error(f"error: {e}")
            return respErrorJson(err=ERROR_SERVICE_ERROR, data=str(e))

    # curl -X 'POST' -i 'http://192.168.98.174:8008/stream' -H 'Content-Type: application/json'  -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJoZWxsbyIsImV4cCI6MTcxMDkxMjM5M30._Oi04sbbPCIdF7TijLF2CdrNNQi4RgXfUQIRwTV8qV0'
    @app.post("/stream")
    async def stream_response(token_base: TokenBase=Depends(get_current_user)):
        print("stream user_id", token_base.user_id)
        async def async_task():
            index=0
            while index < 10:
                data = f"index:{index}\n"
                print(data)
                yield data
                await asyncio.sleep(1)
                index+=1

        return StreamingResponse(async_task(), media_type="text/event-stream")

    # curl -X 'POST' -i 'http://192.168.98.174:8008/streamplus' -H 'Content-Type: application/json'  -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJoZWxsbyIsImV4cCI6MTcxMDkxMjM5M30._Oi04sbbPCIdF7TijLF2CdrNNQi4RgXfUQIRwTV8qV0' 
    @app.post("/streamplus")
    async def stream_response(token_base: TokenBase=Depends(get_current_user)):
        print("stream user_id", token_base.user_id)

        async def async_consumer(queue: Queue[str], indices: list[int], timeout: float):
            indices = set(indices)
            finished = set()
            while indices != finished:
                try:
                    index, response = await asyncio.wait_for(queue.get(), timeout)
                    if response is None:
                        finished.add(index)
                        print("queue_consumer indices finished", indices, finished)
                    yield (index, response)
                except TimeoutError:
                    break

        async def generate_process(index, model, prompt, queue):
            for i in range(50):
                data = f"heavy:{index}_{i}"
                #return data
                # 不能用 yield 返回, 只能用queue
                #yield data
                queue.put_nowait((index, data))
                await asyncio.sleep(1)
            queue.put_nowait((index, None))

        async def async_process(prompt: str, models: List):
            queue = Queue()
            tasks = [
                asyncio.create_task(generate_process(index, model, prompt, queue))
                for index, model in enumerate(models)
            ]

            #all_text = [dict() for _ in range(len(models))]
            #start_time = time.monotonic()
            async for index, response in async_consumer(queue, list(range(len(tasks))), timeout=60):
                if response is not None:
                    # 需要加\n, 否则会一整行返回
                    yield response+"\n"
            
            await asyncio.gather(*tasks, return_exceptions=True)

            #index=0
            #while index < 10:
            #    data = f"index:{index}\n"
            #    print(data)
            #    yield data
            #    await asyncio.sleep(1)
            #    index+=1

        prompt = "hello world"
        models = ["model_1"]
        models.append("model_2")
        models.append("model_3")
        #return StreamingResponse(streaming_generate(prompt=prompt, models=models), media_type="text/event-stream")
        # async_process() 该函数必须是流式返回
        return StreamingResponse(async_process(prompt=prompt, models=models), media_type="text/event-stream")

    logger.info("starting server...")
    uvicorn.run(app=app, host=host, port=port, workers=1)
# end start_server

def parse_args():
    parser = argparse.ArgumentParser(description='Stream API Service demo')
    parser.add_argument('--host', '-H', help='host to listen', default='0.0.0.0')
    parser.add_argument('--port', '-P', help='port of this service', default=8008)
    #parser.add_argument('-c', '--config', type=str, default='config.yaml', help = 'config file')
    args = parser.parse_args()
    
    return args
    
if __name__ == '__main__':
    logger = get_logger(sys.argv[0], 'test.log')
    
    args = parse_args()

    start_server(args.host, int(args.port))
# 生成token
# 创建生成访问令牌的函数
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
    """
    :param data: 需要进行JWT令牌加密的数据(解密的时候会用到)
    :param expires_delta: 令牌有效期
    :return: token
    """
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    # 添加失效时间
    to_encode.update({"exp": expire})
    # SECRET_KEY:密钥
    # ALGORITHM:JWT令牌签名算法
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

路径
获取 token,只返回token字符串
http://lihuaxi.xjx100.cn/news/1207401.html?action=onClick
获取token 返回用户信息
https://blog.csdn.net/baidu_38766791/article/details/127434685

解决 422 Unprocessable Entity
前端react axios 发送post请求fastapi响应报错422 (Unprocessable Entity)
https://blog.csdn.net/bosivip/article/details/127724733

如果curl 请求时不加该参数,也会造成422错误
-H ‘Content-Type: application/json’

返回文件:
https://blog.csdn.net/qq_51116518/article/details/133614101

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