You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

FastAPI接口压测时JWT令牌验证失败问题求助

FastAPI负载测试JWT接口异步报错排查

问题背景

在FastAPI开发接口时,使用Postman进行负载测试发现:带JWT验证的接口出现报错;移除JWT依赖后,压测所有请求均返回成功。所有代码都是同步函数,但报错信息显示存在异步相关问题。

接口代码

@router.get("/task/{id}") # 获取指定任务
def getTask(id:int,db: Session =Depends(get_db),currentUser: dict = Depends(getCurrentUser)):
    print(currentUser)
    task = db.query(Task).filter(Task.id == id).first()
    if task.owner_id == currentUser.id:
        return(task)
    else:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f'requested task id: {id} not available')

JWT处理代码

oauth2_scheme = OAuth2PasswordBearer(tokenUrl='login')

SECRET_KEY = "kjjuhdsfvjkhdkfvkdsfjvhlidhfvidhhsf"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 300

def createToken(data:dict):
    to_encode = data.copy()
    expire = datetime.utcnow() + timedelta(minutes= ACCESS_TOKEN_EXPIRE_MINUTES)

    to_encode.update({"exp":expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

def verifyToken(token:str,credentialException):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get('userid')
        print(f'the logged in user is {username}')
        if username is None:
            raise credentialException
        return username
    except ExpiredSignatureError:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
    except JWTError as e:
        print(e)

def getCurrentUser(token: str = Depends(oauth2_scheme),db : Session =Depends(get_db)):
    credentialException = HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='not authorized',headers={"WWW-Authenticate":"Bearer"})
    token = verifyToken(token,credentialException)

    currentUser = db.query(User).filter(User.email == token).first()

    return(currentUser)

报错信息

ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\anyio\streams\memory.py", line 98, in receive
    return self.receive_nowait()
  File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\anyio\streams\memory.py", line 93, in receive_nowait
    raise WouldBlock
anyio.WouldBlock

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\starlette\middleware\base.py", line 78, in call_next
    message = await recv_stream.receive()
  File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\anyio\streams\memory.py", line 118, in receive
    raise EndOfStream
anyio.EndOfStream

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\uvicorn\protocols\http\h11_impl.py", line 428, in run_asgi
    result = await app(  # type: ignore[func-returns-value]
  File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\uvicorn\middleware\proxy_headers.py", line 78, in __call__
    return await self.app(scope, receive, send)
  File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\fastapi\applications.py", line 282, in __call__
    await super().__call__(scope, receive, send)
  File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\starlette\applications.py", line 122, in __call__
    await self.middleware_stack(scope, receive, send)
  File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\starlette\middleware\errors.py", line 184, in __call__
    raise exc
  File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\starlette\middleware\errors.py", line 162, in __call__
    await self.app(scope, receive, _send)
  File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\starlette\middleware\base.py", line 108, in __call__
    response = await self.dispatch_func(request, call_next)
  File "H:\projects\fastApi-user-login\git code\userLogin\main.py", line 65, in add_process_time_header
    response = await call_next(request)
  File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\starlette\middleware\base.py", line 84, in call_next
    raise app_exc
  File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\starlette\middleware\base.py", line 70, in coro
    await self.app(scope, receive_or_disconnect, send_no_error)
  File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\starlette\middleware\cors.py", line 83, in __call__
    await self.app(scope, receive, send)
  File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\starlette\middleware\exceptions.py", line 79, in __call__
    raise exc
  File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\starlette\middleware\exceptions.py", line 68, in __call__
    await self.app(scope, receive, sender)
  File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\fastapi\middleware\asyncexitstack.py", line 20, in __call__
    raise e
  File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\fastapi\middleware\asyncexitstack.py", line 17, in __call__
    await self.app(scope, receive, send)
  File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\starlette\routing.py", line 718, in __call__
    await route.handle(scope, receive, send)
  File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\starlette\routing.py", line 276, in handle
    await self.app(scope, receive, send)
  File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\starlette\routing.py", line 66, in app
    response = await func(request)
  File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\fastapi\routing.py", line 241, in app
    raw_response = await run_endpoint_function(
  File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\fastapi\routing.py", line 169, in run_endpoint_function
    return await run_in_threadpool(dependant.call, **values)
  File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\starlette\concurrency.py", line 41, in run_in_threadpool
    return await anyio.to_thread.run_sync(func, *args)
  File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\anyio\to_thread.py", line 33, in run_sync
    return await get_asynclib().run_sync_in_worker_thread(
  File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\anyio\_backends\_asyncio.py", line 877, in run_sync_in_worker_thread        
    return await future
  File "H:\projects\fastApi-user-login\git code\userLogin\myenv\lib\site-packages\anyio\_backends\_asyncio.py", line 807, in run
    result = context.run(func, *args)

排查分析

核心原因

  1. 数据库会话线程安全问题:FastAPI会把同步函数放到线程池执行,若get_db依赖未正确实现每个请求独立会话,并发时多个线程共享同一个DB Session,会导致会话状态混乱,尤其JWT验证中也有DB查询,负载高时冲突更明显。
  2. JWT验证异常处理不完整:verifyToken函数捕获JWTError后仅打印错误未抛出异常,导致getCurrentUser可能拿到None,后续代码访问currentUser.id会抛出异常,结合线程池资源耗尽触发EndOfStream。
  3. 线程池资源耗尽:带JWT验证的请求需要额外的DB查询,耗时更长,负载测试时快速占满线程池,AnyIO无法获取空闲线程处理新请求,从而抛出WouldBlock和EndOfStream。

解决方案

1. 修复DB Session依赖实现

确保get_db为每个请求创建独立会话,使用yield自动管理会话生命周期:

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"  # 替换为你的数据库地址

engine = create_engine(
    SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}  # SQLite需加此参数,其他数据库可移除
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

2. 完善JWT验证的异常处理

在verifyToken中捕获JWTError后抛出凭证异常,避免返回None:

def verifyToken(token:str,credentialException):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get('userid')
        print(f'the logged in user is {username}')
        if username is None:
            raise credentialException
        return username
    except ExpiredSignatureError:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token已过期")
    except JWTError as e:
        print(e)
        raise credentialException  # 必须抛出异常,终止请求并返回401

3. 优化性能与并发配置

  • 缓存用户信息:将JWT验证后的用户信息缓存到Redis,避免每次请求都查询数据库,减少数据库压力:
    # 示例:用Redis缓存用户信息
    import redis
    import json
    
    redis_client = redis.Redis(host='localhost', port=6379, db=0)
    
    def getCurrentUser(token: str = Depends(oauth2_scheme),db : Session =Depends(get_db)):
        credentialException = HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='未授权',headers={"WWW-Authenticate":"Bearer"})
        user_id = verifyToken(token,credentialException)
        
        # 先查缓存
        cached_user = redis_client.get(f"user:{user_id}")
        if cached_user:
            return json.loads(cached_user)
        
        # 缓存不存在则查DB
        currentUser = db.query(User).filter(User.email == user_id).first()
        if not currentUser:
            raise credentialException
        
        # 写入缓存,过期时间设为Token有效期
        redis_client.setex(f"user:{user_id}", ACCESS_TOKEN_EXPIRE_MINUTES*60, json.dumps({"id": currentUser.id, "email": currentUser.email}))
        return currentUser
    
  • 调整Uvicorn并发参数:启动时增加工作进程和线程数,提升并发处理能力:
    uvicorn main:app --workers 4 --threads 8
    

4. 检查中间件实现

确保自定义中间件(如报错中的add_process_time_header)是正确的异步函数,避免阻塞事件循环:

# 正确的异步中间件示例
import time
from fastapi import Request

@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    start_time = time.time()
    response = await call_next(request)
    process_time = time.time() - start_time
    response.headers["X-Process-Time"] = str(process_time)
    return response

内容的提问来源于stack exchange,提问作者gowri shankar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 10:37:08