Package: aiohttp-session-ws
Simply put: associate your websockets with a user’s session, and close those connections when you see fit.For example, let’s say you’re usingaiohttp_securityand a user chooses to log in or log out.
Usingaiohttp_session_wsyou can disconnect the open websocket subscriptions associated with their session, and force them to re-connect and re-authorize thier websocket subscriptions.Basic ExampleThe pieces of code in this example are taken from thedemodirectory of this repository.asyncdefhandle_root(request):returnweb.Response(text='Hello world',content_type="text/html")asyncdefhandle_reset(request):session_ws_id=awaitget_session_ws_id(request)response=web.Response(text=f"Reset called on session{session_ws_id}!",content_type="text/plain",)awaitschedule_close_all_session_ws(request,response)awaitnew_session_ws_id(request)returnresponseasyncdefhandle_websocket(request):asyncwithsession_ws(request)aswsr:connected_at=datetime.now()session_ws_id=awaitget_session_ws_id(request)whileTrue:awaitwsr.send_str(f"Websocket associated with session [{session_ws_id}] "f"connected for{(datetime.now()-connected_at).seconds}")awaitasyncio.sleep(1)returnwsrdefmake_app():app=web.Application(middlewares=[aiohttp_session.session_middleware(aiohttp_session.SimpleCookieStorage()),session_ws_middleware,])app.router.add_get("/",handle_root)app.router.add_get("/reset",handle_reset)app.router.add_get("/ws",handle_websocket)setup_session_websockets(app,SessionWSRegistry())returnappUse the code from thedemofolder, which includes a simple template to interact with the websocket in your web-browser.Narrative APIThis package is designed to be straightforward and easy to use.
This lightweight documentation doesn’t attempt to replace the need to read the code, so you’re encouraged to go do exactly that.There are a few moving pieces, but if (and when) you need to do something more complex, you can subclass away.SessionWSRegistryThis is the core ofaiohttp_session_ws.It’s construction is noteworthy:SessionWSRegistry(self, *, id_factory, session_key)id_factorygenerates a session-wide id that associates the websockets.
The default id_factory returns a UUID4, but you can supply your own callable (async callables are supported, too).
the function signature ofid_factoryis:id_factory(request: aiohttp.web.Request)->typing.HashableSo pretty much, return something that can be the key in a dictionary (strings, integers, etc.).session_keyis the name of the key in the session that maps to the session-wide websocket identifier.
By default it’s a sensibleaiohttp_session_ws_id.HelpersYou won’t need to interact withSessionWSRegistrydirectly after you’ve created it, but know that it’s available in youraiohttp.web.Application(access it like this:app['aiohttp_session_ws_registry']).The friendly global manipulators of this object are:get_session_ws_id(request)new_session_ws_id(request)delete_session_ws_id(request)ensure_session_ws_id(request)schedule_close_all_session_ws(request, response)These methods are importable directly fromaiohttp_session_ws.Notice thatschedule_close_all_session_wstakes a response object.
This allows us to end thekeep-alivestatus of the response (viaaiohttp.web.Response.force_close).
This means that as soon as your user has finished receiveing the response, their outstanding websockets will close.This also means that if you have users with re-connecting websockets, you should probably follow this pattern:asyncdefhandle_logout(request):response=web.HTTPFound('/')awaitschedule_close_all_session_ws(request,response)awaitaiohttp_session.new_session(request)awaitnew_session_ws_id(request)returnresponsesession_wsTo track the websockets, you’ll use the async context managersession_ws.
This context manager upgrades the request, and provides itsaiothttp.web.WebSocketResponsecounterpart.
Use if like this:asyncdefhandle_websocket(request):asyncwithsession_ws(request)aswsr:asyncformsginwsr:awaitwsr.send_str(f'Heard:{ws.data}')returnwsrThat’s it. Pretty simple, right?
If you’d like to provide theaiohttp.web.WebSocketResponsewith initialization options (for example, the supported websocket protocols), pass those along tosession_wsas named arguments.asyncdefhandle_websocket(request):asyncwithsession_ws(request,protocols=('graphql-ws',))aswsr:asyncformsginwsr:awaitwsr.send_str(f'Heard:{ws.data}')returnwsrAs mentioned in theNotesbelow, it’s important that your users have asession_ws idprior to attempting a websocket connection (hint: Safari).Use thesession_ws_middlewareto automatically add the key to your sessions.
It should be inside the call-stack ofaiohttp_session.session_middleware:web.Application(middlewares=[aiohttp_session.session_middleware(aiohttp_session.SimpleCookieStorage()),session_ws_middleware,])Finally, to set all of this up, you’ll want to use thesetupmethod (feel encourged to import it assetup_session_ws).Basic usage looks like this:web.Application(middlewares=[aiohttp_session.session_middleware(aiohttp_session.SimpleCookieStorage()),session_ws_middleware,])setup(app,SessionWSRegistry())# <------# etc...returnappNotesWhilesession_wsgenerates anaiohttp_session_ws_idupon connect (if it’s not present), some browsers don’t respectSet-Cookieon a websocket upgrade (e.g. Safari).Therefore it’s best if you ensure that anaiohttp_session_ws_idis present in the users session prior to attempting a websocket connection (if usingaiohttp_session.SimpleCookieStorageoraiohttp_session.EncryptedCookieStorage).If you’re using something more advanced that stores a reference to the session in the session cookie, and stores the actual value server-side (likeaiohttp_session.RedisStorage), then it’s not important whenaiohttp_session_ws_idis set on the cookie, but it is still important that the user has a session cookie prior to a connection attempt.If you want to put the session-ws-id (usuallyaiohttp_session_ws_id) somewhere else in the session, or derive it from the request, you can.
Simply subclassSessionWSRegistryand revise theget_id,set_id, anddelete_idmethods.If you have a cluster of webservers, you’ll need to subclassSessionWSRegistryand revise theregisterandunregisterfunctions so listen on a message broker (for example, usingaioredisand its pubsub feature).