Why would an object's prototype be forgotten?

Viewed 264

Introduction

So I am building a website with node.js, express, express-session, and sequelize.js. Once a user logs in, an instance of the Sequelize model User is created. In my route for user log-in (/auth/login), I have:

var user = (await User.findAll(
        {where: {
            username: username
        }}))[0];

and I few lines down I assign that user to the session.

req.session.user = user;

And then I can persist any changes by simply calling the save method of req.session.user:

await req.session.user.save();

And indeed, if I add this line next:

console.log(Object.getPrototypeOf(req.session.user));

the output is [object SequelizeInstance:User]. So far so good.

Here is the problem

In another route (/users/myaccount/edit-bio) I am able to access the values of req.session.user. That is, the output of

console.log(req.session.user.username);

is seanletendre, as expected. But now when I call

await req.session.user.save();

all I get is the error message:

UnhandledPromiseRejectionWarning: TypeError: req.session.user.save is not a function

"That is weird," I thought, "isn't this the same object?" To investigate, I add the line:

console.log(Object.getPrototypeOf(req.session.user));

just as I did in the log-in route. And what is the output? It is: [object Object]. So it seems that somehow the prototype of req.session.user gets forgotten. I don't understand how this can be.

Is it possible to re-assign a prototype to a plain object?

Suspect A

Based on the comments to my question, I suspect that the prototype is lost when the session manager serializes req.session. It seems that, unlike I thought before,req.session does not point to the exact same session object for different requests. Each time a request ends, it serializes and stores req.session. Then upon receiving a new request with a cookie designating it as part of the same session, the session object is fetch from the session store.

This is how my session middleware is setup:

var session = require('express-session');
//
// yada, yada, yada
//
app.use(session({
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: {secure: true}
}));

So what surprises me is that, even though I am using the default store MemoryStore, my sessions are still serialized.

My question now becomes: how can I prevent object serialization upon session store when using MemoryStore?

2 Answers

In express-session the method save() is exposed by the object session into the request object (docs), eg.:

req.session.save(callback)

Your code req.session.user.save() is wrong, the correct way is req.session.save(), diff.:

req.session.user.save();
-----------^^^^^
req.session.save()

The method save() isn't a Promise, you must pass a callback for wait for the result of the save:

req.session.user = user;
req.session.save(function(err) {
  if( err ){
    // session not saved
  } else {
    // session saved
  }
})

you can transform it into a Promise (and await it), in this way:

const saveSession = (req) => {
  return new Promise((resolve, reject) => {
    req.session.save(function(err) {
      if( err ){
        reject(err);
      } else {
        resolve(true);
      }
    });
  });
};

req.session.user = user;
await saveSession(req);

The method save() is automatically called at the end of the HTTP response if the session data has been altered. Because of this, typically this method does not need to be called.

UPDATE

I call req.session.user.save() because I want to save the Sequelize Model instance into my database.

express-session use JSON.stringify() for serialize the session into a string and store into a session storage (more info). JSON.stringify() doesn't understand functions and only property are stored. For this reason, your save() function is lost.

Is it possible to re-assign a prototype to a plain object?

Technically you could re-assign the .prototype and .constructor. However, a model instance is quite a complex object. Try logging it to debugging console and you'll see it has lots of sub-objects of its own kind and also references to other more distant objects residing in a "more global" space (nothing like the plain object after deserialization (of JSON basically)). You'd have to re-instanciate them all. That is very hard to do.

My question now becomes: how can I prevent object serialization upon session store when using MemoryStore?

I am not experienced with express-session, but from what i read i think it basically works with parsing and stringifying JSON. So you can't prevent it when using express-session.

What you could do:

  1. Just store the user.id to req.session.user_id. And User.findOne({ where: { id }) upon reloading the session. You then get a "real" sequelize model instance. And that instance you could assign to req.user, so that it's available throughout the request.
  2. If you also hoped to avoid reloading the user's instance upon every request, then you are searching for a "caching" solution, which is different from "sessions". You'd then use a caching system like memcache, redis or others. However, there too, you probably won't get back a "real" instance of the model from the caching server.
  3. If you have only one server where your application is running on, you could have a global.users variable, where you store all user instances via their id. For example "global.users[12345] = user". Like with 1. you'd retrieve it (or when not found initially load it) based on session.user_id

Some more thoughts: In your place, i'd also invest some time in figuring out, if sequelize allows to populate a new User() instance with data from JSON, and tricking sequelize into believing that this is not a new record. Also, keep in mind, that depending on your application, the user's data in the database can change, while in your session or global.users you miss these changes.

Related