LeSeulArtichaut
LeSeulArtichaut

Reputation: 93

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?

Upvotes: 4

Views: 2231

Answers (2)

Martijn Pieters
Martijn Pieters

Reputation: 1123400

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!

Upvotes: 1

Tarun Lalwani
Tarun Lalwani

Reputation: 146610

You yourself change the order. The code should be like this

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

    web.run_app(app)

If you look at the code for aiohttp_session.setup

https://github.com/aio-libs/aiohttp-session/blob/master/aiohttp_session/init.py

def setup(app, storage):
    """Setup the library in aiohttp fashion."""

    app.middlewares.append(session_middleware(storage))

As you can see the middleware is added in this function. Adding your middleware before middleware.setup(app) makes the session still not available for the request

Upvotes: 1

Related Questions