Tortoise crashes when used with Vercel and FastAPI
Thytu opened this issue · 1 comments
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)
Update: I found this temporary fix, however I'll obviously need to find a better option :D
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
await init_db() # already in the previous snippet
response = await call_next(request)
await Tortoise.close_connections() # Now also close the session after each call
return response