Closed virajkanwade closed 6 months ago
It is a manifestation of the starlette BaseHTTPMiddleware. https://github.com/supertokens/supertokens-python/blob/master/supertokens_python/framework/fastapi/fastapi_middleware.py#L28
See https://github.com/tiangolo/fastapi/discussions/6985#discussioncomment-8989601
Ran a simple test
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.requests import Request
from starlette.types import ASGIApp, Receive, Scope, Send
class AsgiMiddleware:
def __init__(self, app: ASGIApp) -> None:
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
await self.app(scope, receive, send)
class StarletteHTTPMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
return await call_next(request)
With app.add_middleware(AsgiMiddleware)
With app.add_middleware(StarletteHTTPMiddleware)
Tried to create a asgi middleware replacement for supertokens fastapi middleware. (its very late for me, sorry if I messed up something.)
from typing import Any, Dict, Union
from starlette.requests import Request
from starlette.responses import Response
from starlette.types import ASGIApp, Message, Receive, Scope, Send
from supertokens_python import Supertokens
from supertokens_python.exceptions import SuperTokensError
from supertokens_python.framework import BaseRequest, BaseResponse
from supertokens_python.framework.fastapi.fastapi_request import (
FastApiRequest,
)
from supertokens_python.framework.fastapi.fastapi_response import (
FastApiResponse,
)
from supertokens_python.recipe.session import SessionContainer
from supertokens_python.supertokens import manage_session_post_response
from supertokens_python.utils import default_user_context
def get_middleware():
class AsgiMiddleware:
def __init__(self, app: ASGIApp) -> None:
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] != "http":
await self.app(scope, receive, send)
return
async def _manage_session_post_response(
request: BaseRequest, result: BaseResponse, user_context: Dict[str, Any]
):
if hasattr(request.state, "supertokens") and isinstance(
request.state.supertokens, SessionContainer
):
manage_session_post_response(
request.state.supertokens, result, user_context
)
async def send_wrapper(message: Message) -> None:
await send(message)
if message["type"] == "http.response.start":
response.response.status_code = message["status"]
if "headers" in message:
response.response.init_headers(
{
k.decode("utf-8"): v.decode("utf-8")
for k, v in message["headers"]
}
)
elif message["type"] == "http.response.body":
response_body += message["body"]
more_body = message.get("more_body", False)
# If the body is not complete yet, return to wait
if more_body:
return
response.response.body = response.response.render(
response_body.decode("utf-8")
)
await _manage_session_post_response(request, response, user_context)
return
st = Supertokens.get_instance()
request = Request(scope)
custom_request = FastApiRequest(request)
user_context = default_user_context(custom_request)
try:
response = FastApiResponse(Response())
result: Union[BaseResponse, None] = await st.middleware(
custom_request, response, user_context
)
if result is None:
response = FastApiResponse(Response())
response_body = b""
await self.app(scope, receive, send_wrapper)
return
else:
await _manage_session_post_response(request, result, user_context)
if isinstance(result, FastApiResponse):
await result.response(scope, receive, send)
return
return
except SuperTokensError as e:
response = FastApiResponse(Response())
error_result: Union[BaseResponse, None] = await st.handle_supertokens_error(
FastApiRequest(request), e, response, user_context
)
if isinstance(error_result, FastApiResponse):
await error_result.response(scope, receive, send)
return
raise Exception("Should never come here")
return AsgiMiddleware
Thanks for opening this issue @virajkanwade . We will have a look at this in a few week's time
https://medium.com/@robbe.sneyders/redesigning-connexion-as-an-asgi-middleware-stack-a5dc17e81ff8
might help on some of the nuances of asgi middleware
hi @virajkanwade ,
From the stats you have mentioned:
Without Supertokens middleware, each request takes about 0.09 ms and with Supertokens middleware, each request takes about 0.21 ms
This means that the supertokens adds an overhead of about 0.12 ms per request. Which means, 1.2 sec loss on 10000 requests. Does that make a huge difference?
How does your test fare if you had a db call in the API?
@sattvikc you are looking at it from response time side.
I am looking at it from infrastructure side.
FastAPI by default can handle around 10,000 req/sec.
To support 100,000 req/sec, I need to run 10-11 FastAPI servers.
With the supertokens middleware (using Starlette BaseHTTPMiddleware), it goes to around 4,800 req/sec. To support 100,000 req/sec, I would need to run 20-21 servers.
With the supertokens middleware reimplemented as ASGI middleware (https://github.com/supertokens/supertokens-python/issues/503#issuecomment-2106153534), it could support little over 8,200 req/sec. To support 100,000 req/sec, I would need to run 13-14 servers.
Now scale that up to 200,000 req/sec, 500,000 req/sec.
Its about optimizing performance.
@sattvikc Also do look at https://github.com/encode/starlette/discussions/2160#discussioncomment-6053384
I’m warming up to the idea of not removing it for 1.0. I still think we should discourage its use, but maybe we can keep it around until 2.0 to ease the burden for migrating to 1.0? Does something like this even make sense or if we’re going to discourage it should we just remove it?
This has been implemented and released in python SDK version >= 0.20.2. Thanks @virajkanwade .
Feel free to add yourself to the supertokens-core repo readme as a contributor :)
core running on docker
Created 2 hello world FastAPI apps, one vanilla, other with supertokens.
benchmark for vanilla app:
benchmark with supertokens middleware
Is this big difference expected?
Thanks