Search code examples
pythonmiddlewareaiohttp

Is it possible to access a session (from aiohttp_session) within a middleware?


I'm setting up an aiohttp server using aiohttp_session to store data into an EncryptedCookieStorage. I use it to store a 7-days valid token, along with the expiration date and a refresh token. I want, no matter which endpoint the client is accessing, to check if the token (stored in the session) needs some refreshment. The choice of a middleware was pretty obvious.

The problem is, when I call await aiohttp_session.get_session(request), I'm getting a nice RuntimeError asking me to setup the aiohttp_session middleware to the aiohttp.web.Application. My guess is that my custom middleware was called before the one handling the session loading, thus the session is not accessible yet. I've searched for some "priority" system regarding middlewares, but haven't found anything.

My server is set up in a main.py file like:

def main():
    app = web.Application()
    middleware.setup(app)
    session_key = base64.urlsafe_b64decode(fernet.Fernet.generate_key())
    aiohttp_session.setup(app, EncryptedCookieStorage(session_key))
    # I have tried swapping the two setup functions

    web.run_app(app)


if __name__ == '__main__':
    main()

Where the middleware.setup() is in a separate package, in the __init__.py:

# For each python file in the package, add it's middleware function to the app middlewares
def setup(app):
    for filename in listdir('middleware'):
        if filename[-2:] == 'py' and filename[:2] != '__':
            module = __import__('rpdashboard.middleware.' + filename[:-3], fromlist=['middleware'])
            app.middlewares.append(module.middleware)

And finally, the middleware I want to get the session in is:

@web.middleware
async def refresh_token_middleware(request, handler):
    session = await get_session(request)
    if session.get('token'):
        pass  # To be implemented ...

    return await handler(request)


middleware = refresh_token_middleware

The execution issues here:

# From aiohttp_session
async def get_session(request):
    session = request.get(SESSION_KEY)
    if session is None:
        storage = request.get(STORAGE_KEY)
        if storage is None:
            # This is raised
            raise RuntimeError(
                "Install aiohttp_session middleware "
                "in your aiohttp.web.Application")

As I was saying earlier, it seems like the session is not meant to be accessed in a middleware, and isn't loaded yet. So how would I prevent my custom middleware to run before the session loading one? Or maybe simply run manually the aiohttp_session middleware myself? Is it even possible?


Solution

  • Yes, middleware components added to the app in the right order can access the session storage set by the session middleware.

    The aiohttp documentation covers the priority order for middleware components in their Middlewares section:

    Internally, a single request handler is constructed by applying the middleware chain to the original handler in reverse order, and is called by the RequestHandler as a regular handler.

    Further down, they use an example to demonstrate what this means. In summary, they use two middleware components that report their entry and exit, and add them to the app.middlewares list in this order:

    ... middlewares=[middleware1, middleware2]
    

    This ordering produces the following output:

    Middleware 1 called
    Middleware 2 called
    Handler function called
    Middleware 2 finished
    Middleware 1 finished
    

    So an incoming request is passed along the different middleware in the same order they are added to the app.middlewares list.

    Next, aiohttp_session also documents how they add their session middleware, in the API entry for aiohttp_session.setup():

    The function is shortcut for:

    app.middlewares.append(session_middleware(storage))
    

    So their middleware component is added to the end of the list. Per above that means that anything that requires access to the session must come after this middleware component.

    All that the session middleware does is add the storage to the request under the aiohttp_session.STORAGE_KEY key; this makes the sessions available to any further middleware components that follow it. Your middleware components do not need to do anything special other than be added after the session middleware and leave the storage object added to the request in place. The request object is designed to share data between components this way.

    Your code puts all your middleware components before the session middleware component:

    middleware.setup(app)
    # ...
    aiohttp_session.setup(app, EncryptedCookieStorage(session_key))
    

    This gives you an ordering of [..., refresh_token_middleware, ..., session_middleware] and your middleware can’t access any session information.

    So you have to swap the order; call aiohttp_session.setup() first, and only then add your own components:

    aiohttp_session.setup(app, EncryptedCookieStorage(session_key))
    middleware.setup(app)
    

    If you still have issues accessing the session storage then that means one of the intervening middleware components is removing the session storage information again.

    You could use the following middleware factory at various locations to report on the session storage being present to help you debug this:

    from aiohttp import web
    from aiohttp_session import STORAGE_KEY
    
    COUNTER_KEY = "__debug_session_storage_counter__"
    _label = {
        False: "\x1b[31;1mMISSING\x1b[0m",
        True: "\x1b[32;1mPRESENT\x1b[0m",
    }
    
    def debug_session_storage(app):
        pre = nxt = ""
        if app.middlewares:
            previous = app.middlewares[-1]
            name = getattr(previous, "__qualname__", repr(previous))
            pre = f" {name} ->"
            nxt = f" {name} <-"
    
        @web.middleware
        async def middleware(request, handler):
            counter = request.get(COUNTER_KEY, -1) + 1
            request[COUNTER_KEY] = counter
            found = STORAGE_KEY in request
            indent = " " * counter
            print(f"{indent}-{pre} probe#{counter} - storage: {_label[found]}")
            try:
                return await handler(request)
            finally:
                print(f"{indent}-{nxt} probe#{counter} - done")
    
        app.middlewares.append(middleware)
    

    If you insert this between every piece of middleware you add you should be able to figure out if and where the session storage is being lost:

    def setup(app):
        # start with a probe
        debug_session_storage(app)
    
        for filename in listdir('middleware'):
            if filename[-2:] == 'py' and filename[:2] != '__':
                module = __import__('rpdashboard.middleware.' + filename[:-3], fromlist=['middleware'])
    
                app.middlewares.append(module.middleware)
    
                # Add debug probe after every component
                debug_session_storage(app)
    

    This should tell you

    • what middleware component preceded each probe
    • if the session storage is present, using ANSI green and red colours to make it easy to spot
    • if any have reset the request entirely; if the probe counts start at 0 again then something cleared not only the session key but the probe counter as well!