tortoise / tortoise/tortoise-orm

Tortoise crashes when used with Vercel and FastAPI

Open
#1,684 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
5.6k
Forks
516
Avg merge
2d 21h
Merged PRs (30d)
9

Description

Describe the bug
Tortoise seems to crash when used with Vercel (and probably any serverless service) + FastAPI.

When using tortoise-orm with FastAPI and Vercel, tortoise cannot manage to fetch the database connection resulting in the following error:

TypeError: 'NoneType' object is not iterable
Traceback (most recent call last):
  File "/var/task/vc__handler__python.py", line 315, in vc_handler
    response = asgi_cycle(__vc_module.app, body)
  File "/var/task/vc__handler__python.py", line 215, in __call__
    asyncio.run(self.run_asgi_instance(asgi_instance))
  File "/var/lang/lib/python3.12/asyncio/runners.py", line 194, in run
    return runner.run(main)
  File "/var/lang/lib/python3.12/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
  File "/var/lang/lib/python3.12/asyncio/base_events.py", line 687, in run_until_complete
    return future.result()
  File "/var/task/vc__handler__python.py", line 219, in run_asgi_instance
    await asgi_instance
  File "/var/task/fastapi/applications.py", line 1054, in __call__
    await super().__call__(scope, receive, send)
  File "/var/task/starlette/applications.py", line 123, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/var/task/starlette/middleware/errors.py", line 186, in __call__
    raise exc
  File "/var/task/starlette/middleware/errors.py", line 164, in __call__
    await self.app(scope, receive, _send)
  File "/var/task/starlette/middleware/cors.py", line 85, in __call__
    await self.app(scope, receive, send)
  File "/var/task/starlette/middleware/exceptions.py", line 65, in __call__
    await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
  File "/var/task/starlette/_exception_handler.py", line 64, in wrapped_app
    raise exc
  File "/var/task/starlette/_exception_handler.py", line 53, in wrapped_app
    await app(scope, receive, sender)
  File "/var/task/starlette/routing.py", line 756, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/var/task/starlette/routing.py", line 776, in app
    await route.handle(scope, receive, send)
  File "/var/task/starlette/routing.py", line 297, in handle
    await self.app(scope, receive, send)
  File "/var/task/starlette/routing.py", line 77, in app
    await wrap_app_handling_exceptions(app, request)(scope, receive, send)
  File "/var/task/starlette/_exception_handler.py", line 64, in wrapped_app
    raise exc
  File "/var/task/starlette/_exception_handler.py", line 53, in wrapped_app
    await app(scope, receive, sender)
  File "/var/task/starlette/routing.py", line 72, in app
    response = await func(request)
  File "/var/task/fastapi/routing.py", line 278, in app
    raw_response = await run_endpoint_function(
  File "/var/task/fastapi/routing.py", line 191, in run_endpoint_function
    return await dependant.call(**values)
  File "/var/task/Manager/main.py", line 48, in some_example
    await Video.all() # KO
  File "/var/task/tortoise/models.py", line 1262, in all
    return cls._db_queryset(using_db)
  File "/var/task/tortoise/models.py", line 1063, in _db_queryset
    db = using_db or cls._choose_db(for_write)
  File "/var/task/tortoise/models.py", line 1011, in _choose_db
    db = router.db_for_read(cls)
  File "/var/task/tortoise/router.py", line 36, in db_for_read
    return self._db_route(model, "db_for_read")
  File "/var/task/tortoise/router.py", line 31, in _db_route
    return connections.get(self._router_func(model, action))
  File "/var/task/tortoise/router.py", line 18, in _router_func
    for r in self._routers:

To Reproduce
Here is the code-snippet to reproduce it:

# app/main.py
from fastapi import FastAPI
from tortoise import Tortoise
from contextlib import asynccontextmanager
from tortoise.contrib.fastapi import RegisterTortoise
from tortoise import models, fields


# NOTE: this must be defined in
# a different file and then imported
class SomeTable(models.Model):
    id = fields.IntField(pk=True)


@asynccontextmanager
async def lifespan(app: FastAPI):
    async with RegisterTortoise(
        app=app,
        config=YOUR_CONFIG_HERE,
        generate_schemas=True,
        add_exception_handlers=True,
    ):
        yield

    await Tortoise.close_connections()


app = FastAPI(lifespan=lifespan)


@app.get("/some-example")
async def some_example():
    await SomeTable.all() # TypeError: 'NoneType' object is not iterable
    return "some expected response"


if __name__ == "__main__":
    import uvicorn

    uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

Obviously, in order to reproduce the error one also needs to deploy it on Vercel :)

Expected behavior
I would expect tortoise to use previously initialized connection to return every elements of SomeTable.

Additional context
Note that in order to debug I also tried the following, which also results in a crash. However this time it crashs at the second call to the database as it tried to close an event loop that no-longer exists, resulting in a RuntimeError('Event loop is closed') error.

# app/main.py
from fastapi import FastAPI
from tortoise import Tortoise
from contextlib import asynccontextmanager
from tortoise.contrib.fastapi import RegisterTortoise
from tortoise import models, fields


# NOTE: this must be defined in
# a different file and then imported
class SomeTable(models.Model):
    id = fields.IntField(pk=True)


async def init_db():
    await Tortoise.init(config=YOUR_CONFIG_HERE)
    await Tortoise.generate_schemas()


app = FastAPI()


@app.get("/some-example")
async def some_example():
    await init_db() # First call works
    # second calls fail with `raise RuntimeError('Event loop is closed')``
    await SomeTable.all()
    return "some expected response"


if __name__ == "__main__":
    import uvicorn

    uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with app/main.py and the FastAPI lifespan example, then inspect RegisterTortoise, Tortoise.init, and Tortoise.close_connections as used in the report. Reproduce the endpoint on Vercel and verify that repeated requests use the initialized connection without the NoneType router error or an Event loop is closed failure.

Written by the indexing model from the issue text.

Assessment

Tech stack
fastapi, python
Domain
backend, databases
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.