Search code examples
javascriptnode.jsexpressexpress-session

Is it possible to change the Express Session to a different store after creation


I am using memcached as a backing store for an ExpressJS session - chosen via an app configuration setting. I would like to fall back from memcached to memory if the memcached host cannot be contacted. (This isn't necessarily a production strategy as memcached is very reliable - it's more for when I forget to boot the Docker instance in dev, but could still be a production fail-safe.)

I first thought I could "app.use" a new session instance, and try to remove the first one, but I have read that it's difficult (if possible) to "un-use" Express middleware i.e. to swap the middleware in-place in the chain, or generally tinker with the chain once it's been setup.

The problem there is after the connection timeout period, the app middleware has been setup with many further services installed after the session middleware.

My second thought was can I re-configure the Express Session instance itself and change the store after it's been created? I could not see any way in the documentation.

My third idea was to wrap the express-session in a new "swappable store" class, but I'm wary of the scope of wrapping the entire interface.

For example in my app setup:

app.use(services.session.middleware());

// then a lot of other middleware...
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));
app.use(cookieParser());
app.use(express.static(path.join(rootPath, 'public')));
...etc

And in the session service, which selects and configures the session instance:

function middleware() {
    // default / Memory Store options
    const opts = {
        resave: false,
        saveUninitialized: false,
        ...etc
        }
    };

    // Install any configured backing store
    if(config.session.storage === "memcached"){
        const MemcachedStore = require('connect-memcached')(session);
        opts.proxy = 'true';
        opts.store = new MemcachedStore({
            hosts: ...,
            secret: ... 

        });
        const errorHandler = (type, details)=> {
            /* HERE I WOULD LIKE TO RE-MAKE THE SESSION USING MEMORY
             * AND DISCARD THE MEMCACHED ONE
             * (THIS HAPPENS AFTER APP BOOT HAS FINISHED)
            */
            console.error( String.format("Memcached {3} with host:{0} details:{1}.", details.server, details.messages.join( '' ), type)); 
        }
        opts.store.client.on('failure', details => errorHandler('failure', details));
        opts.store.client.on('issue', details => errorHandler('issue', details));
    }

    return session(opts);
}

Solution

  • I think there are several approaches you can try for this. The first is to configure a storage system based on which configuration you have supplied on startup (probably via a module like config or nconf). The second is to run a quick check when booting up the app to make sure it can access the memcache service, and if it can't then fallback to memory with an error.

    I would be fairly weary of doing either of these, since you're using docker and it should be easy to boot memcache. This is because you'll be introducing code which might trigger in production should there be some connection issue, and then you might find yourself accidentally serving sessions out of memory rather than something like memcache potentially without realising.

    I'll expand on both strategies here and provide a third possibly better option.

    1. Choose the cache system based on a config

    This should be fairly straight forward, simply extract your configuration into some sort of config manager / environment variables (checkout config or nconf). When starting the application and connecting your session middleware, you can pull out all the possibly configurations, see which exist and attach one based on that. This is similar to how your if (config.session.storage === 'memcache") looks at the moment. Just use a fallback of not configuring one and the express-session middleware will fall back to memory. This way you can leave out the configuration completely and just always use memory for development.

    2. Run a test before connecting to the desired service

    In combination with the above, if memcache details are provided you could run a quick test by attempting to store something in memcache on startup. Perhaps new Date(); to signal when the application booted up? If this throws an error, then just don't attach the MemcachedStore to the express-session options and you can safely destroy the MemcachedStore.

    3. Throw an error if you cannot connect to Memcached

    This is in further combination to #2. If you identify that memcache configurations are provided, then I would personally do a check to see if you can contact the serivce and if not then throw an error and stop the application. This would mean that in development you immedietely know the issue, and in production you would as well and can trigger automatic alerts for yourself based on the fact that the application failed to start.

    This is probably the best and most robust solution, generally doing a silent fallback is not a great idea when talking about connected services as things can go wrong and you have no idea. I appreciate that this is for development purposes and you need to be pragmatic, but if it saves you accidentally serving all sessions from your servers memory then this would be super beneficial.