fastapi: Problems with await in async dependencies (when using dependencies and middleware simultaneously)

First Check

  • I added a very descriptive title to this issue.
  • I used the GitHub search to find a similar issue and didn’t find it.
  • I searched the FastAPI documentation, with the integrated search.
  • I already searched in Google “How to X in FastAPI” and didn’t find any information.
  • I already read and followed all the tutorial in the docs and didn’t find an answer.
  • I already checked if it is not related to FastAPI but to Pydantic.
  • I already checked if it is not related to FastAPI but to Swagger UI.
  • I already checked if it is not related to FastAPI but to ReDoc.

Commit to Help

  • I commit to help with one of those options 👆

Example Code

import fastapi
import datetime as dt
import sqlalchemy as sa

from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.orm import sessionmaker


class _Conn:
    radius_engine: AsyncEngine
    radius_session_maker: sessionmaker


def init_pg_connections() -> None:
    _Conn.radius_engine = create_async_engine(
        'postgresql+asyncpg://postgres:password@host:port/db_name',
        connect_args={
            'server_settings': {
                'application_name': 'test_app'
            }
        },
        pool_size=2, max_overflow=10
    )
    _Conn.radius_session_maker = sessionmaker(
        bind=_Conn.radius_engine,
        autoflush=False, autocommit=False,
        expire_on_commit=False,
        class_=AsyncSession
    )


async def get_db():
    db: AsyncSession = _Conn.radius_session_maker()
    try:
        yield db
    finally:
        print('finally get_db. Before close')
        await db.close()
        print('finally get_db. After close')


def get_app():
    app = fastapi.FastAPI(
        on_startup=[init_pg_connections]
    )

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

    @app.post('/test')
    async def test(
            data: int = fastapi.Body(5, embed=True),
            db: AsyncSession = fastapi.Depends(get_db)
    ):
        res = (await db.execute(sa.text('Select * FROM radacct'))).scalars().all()
        print('test', f'{data=} {res=}')
        return 'OK'
    return app


if __name__ == '__main__':
    import uvicorn
    _app = get_app()
    uvicorn.run(_app, host='10.10.10.98', port=7776)

Description

FastAPI version>=0.74 has a very strange problem with dependency (func get_db in example).

  1. If you run this code as it is and send request from swagger every thing is ok (Connection with db will close and you see message ‘finally get_db. After close’ in console).

  2. If you run this code as it is and use curl or aiohttp to send request, then “await db.close()” is never finish and you will not see message ‘finally get_db. After close’ in console. And if you continue to send requests, then you get warnings from Garbage collector and SQLAlchemy:

The garbage collector is trying to clean up connection <AdaptedConnection <asyncpg.connection.Connection object at 0x7fd3626a4740>>. This feature is unsupported on async dbapi, since no IO can be performed at this stage to reset the connection. Please close out all connections when they are no longer used, calling "close()" or using a context manager to manage their lifetime.

/usr/lib/python3.10/ipaddress.py:45: SAWarning: The garbage collector is trying to clean up connection <AdaptedConnection <asyncpg.connection.Connection object at 0x7fd3626a4740>>. This feature is unsupported on async dbapi, since no IO can be performed at this stage to reset the connection. Please close out all connections when they are no longer used, calling "close()" or using a context manager to manage their lifetime.

return IPv4Address(address)

  1. If you comment middleware and send requests from any client (swagger, curl and aiohttp), then every thing is ok.

  2. If you run this code as it is with FastAPI version <= 0.73, then every thing is ok.

In each cases clients has no problems in getting 200 responses from server. Unfortunately i have no idea why this happening.

Requirements: anyio==3.5.0 asgiref==3.5.0 asyncpg==0.25.0 click==8.0.4 coloredlogs==15.0.1 fastapi==0.75.0 | 0.73.0 greenlet==1.1.2 h11==0.13.0 httptools==0.3.0 humanfriendly==10.0 idna==3.3 mypy==0.931 mypy-extensions==0.4.3 pydantic==1.9.0 python-dateutil==2.8.2 python-dotenv==0.19.2 PyYAML==6.0 six==1.16.0 sniffio==1.2.0 SQLAlchemy==1.4.32 sqlalchemy2-stubs==0.0.2a20 starlette==0.17.1 tomli==2.0.1 typing_extensions==4.1.1 uvicorn==0.17.5 uvloop==0.16.0 watchgod==0.7 websockets==10.2

Operating System

Linux

Operating System Details

No response

FastAPI Version

=0.74

Python Version

3.9 and 3.10

Additional Context

No response

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 9
  • Comments: 35 (9 by maintainers)

Most upvoted comments

Same here with async_session used inside a context manager, pinning fastapi to 0.73.0 fixes it. Using asyncmy as driver.

async def get_db_session(request: Request) -> AsyncGenerator[AsyncSession, None]:
    async_sessionmaker = request.app.state.sessionmaker
    session: AsyncSession
    async with async_sessionmaker() as session:
        try:
            yield session
        except SQLAlchemyError:
            await shield(session.rollback())
            logger = get_logger()
            logger.exception("sqlalchemy_exception")

BaseHTTPMiddleware is fundamentally broken, I would not count on it being fixed.

Adrian, I followed your advise, that is, ditched @app.middleware("http") and rewrote my middleware as generic ASGI middleware. Haven’t seen the error since. Thank you so much!

Yes, having the same problem with get_db dependency not closing connection and the same error message. And it started with 0.74 release https://github.com/tiangolo/fastapi/releases/tag/0.74.0

from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker

from app.config import settings


engine = create_async_engine(
    settings.DB_DSN,
    echo=settings.DB_ECHO,
    pool_size=settings.DB_POOL_SIZE,
    max_overflow=settings.DB_MAX_OVERFLOW,
    future=True,
)
async_session = sessionmaker(engine, expire_on_commit=False, class_=AsyncSession, future=True, autoflush=False)


async def get_db():
    db = async_session()
    try:
        yield db
    finally:
        await db.close()

And I have middlewares also