tortoise / tortoise/tortoise-orm
Tortoise crashes when used with Vercel and FastAPI
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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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