alex-oleshkevich / starsessions

Advanced sessions for Starlette and FastAPI frameworks
MIT License
98 stars 11 forks source link

Proposal: `starsessions.SessionStore.write` should be passed the `starlette.types.Message` #74

Closed ppena-LiveData closed 4 months ago

ppena-LiveData commented 5 months ago

Our custom starsessions.SessionStore has different processing depending on the status code of the response, but there's no easy way for us to get that status code. To workaround that, we had to create a starlette middleware just to inject the status code. If starsessions.SessionStore.write was passed the starlette.types.Message, then the store would be able to get the message['status'] directly without us needing a separate middleware class to get it.

Here's a simplified example of what we currently have to do:

from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.responses import JSONResponse
from starlette.routing import Route
from starlette.testclient import TestClient
from starlette.types import ASGIApp, Message, Receive, Scope, Send
from starsessions import InMemoryStore, SessionMiddleware, load_session

async def index_view(request):
    await load_session(request)

    session_data = request.session
    session_data['new_test_val'] = 42
    return JSONResponse(session_data)

class MyStore(InMemoryStore):
    async def write(self, session_id: str, data: bytes, lifetime: int, ttl: int) -> str:
        if self.hack_for_status_code <= 400:
            self.data[session_id] = data
        return session_id

session_store = MyStore()

# this class wouldn't be needed if the starlette.types.Message was passed to the starsessions store
class InterceptMiddlewareForStatusCode:
    def __init__(self, app: ASGIApp, store: MyStore):
        self.app = app
        self.store = store

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        if scope['type'] != 'http':
            await self.app(scope, receive, send)
            return

        async def send_wrapper(message: Message) -> None:
            if message['type'] == 'http.response.start':
                self.store.hack_for_status_code = message['status']
            await send(message)

        await self.app(scope, receive, send_wrapper)

app = Starlette(
    middleware=[
        Middleware(SessionMiddleware, store=session_store),
        Middleware(InterceptMiddlewareForStatusCode, store=session_store),
    ],
    routes=[
        Route('/', index_view),
    ]
)

with TestClient(app) as client:
    client.get('/')
alex-oleshkevich commented 4 months ago

Your middleware approach is a valid solution. You don't need a custom store since you can get session from scope['session'] and then you can modify session contents as you wish.

Anyway, I don't want HTTP context to leak into sessions as the only thing that connects session store and HTTP request is cookie name.