Hybrid application: move the hot handlers to aiohttp¶
An ASGI application pays for the ASGI bridge and for the framework which it
runs on. An aiohttp handler writes to the response directly, therefore it
does less work for each request. A full rewrite of a large FastAPI
application, however, costs much time and gives much risk.
This package makes the rewrite optional: keep the ASGI application for every route, and rewrite only the handlers which need the speed. The server stays one process, and the ASGI application keeps its routes, dependencies and lifespan.
The pattern¶
Register the rewritten
aiohttphandlers first.Register the ASGI application as a catch-all resource after them.
aiohttp searches the resources in the registration order, therefore the
rewritten paths go to the aiohttp handlers, and every other path goes to the
ASGI application.
from aiohttp import web
from fastapi import FastAPI
from aiohttp_asgi import ASGIResource
asgi_app = FastAPI()
@asgi_app.get("/users/{user_id}")
async def get_user(user_id: int):
"""This route stays on FastAPI."""
return {"user_id": user_id, "served_by": "fastapi"}
@asgi_app.get("/metrics")
async def metrics():
"""This route gets a faster aiohttp handler below."""
return {"requests": 0, "served_by": "fastapi"}
async def metrics_handler(request: web.Request) -> web.Response:
"""The rewritten handler. It answers without the ASGI bridge."""
return web.json_response({"requests": 0, "served_by": "aiohttp"})
aiohttp_app = web.Application()
# 1. The rewritten handlers.
aiohttp_app.router.add_get("/metrics", metrics_handler)
# 2. The ASGI application for everything else.
asgi_resource = ASGIResource(asgi_app)
aiohttp_app.router.register_resource(asgi_resource)
asgi_resource.lifespan_mount(aiohttp_app)
if __name__ == "__main__":
web.run_app(aiohttp_app)
curl http://127.0.0.1:8080/metrics
# {"requests": 0, "served_by": "aiohttp"}
curl http://127.0.0.1:8080/users/42
# {"user_id":42,"served_by":"fastapi"}
The FastAPI route /metrics stays in the code. It documents the answer, it
serves the tests of the ASGI application, and it answers again when the
aiohttp route goes away.
Warning
Register the aiohttp routes before a resource with the default
root_path="/". aiohttp reuses the last added resource when its
raw_match() accepts the path, therefore a router.add_get() call after the
catch-all resource raises
AttributeError: 'ASGIResource' object has no attribute 'add_route'.
Keep the answers equal¶
A rewritten handler must answer like the ASGI route which it replaces, because the clients do not see the difference. Useful checks:
The status code, the headers and the JSON structure.
The validation errors. FastAPI answers 422 with a body which describes the problem; an
aiohttphandler must answer in the same form when the clients read it.The authentication. A FastAPI dependency which checks a token must become an
aiohttpmiddleware or a function which the new handler calls.
Keep a test for each rewritten path, and run it against both answers while the rewrite continues.
Measure before the rewrite¶
Rewrite the handlers which the profile shows, not the handlers which look slow. A handler which waits for a database gives little to a rewrite, because the time goes to the database. A handler which serializes much data, or which answers very often, gives more.