Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

lazy sessions feature #822

Closed
wants to merge 1 commit into from

3 participants

@carlos8f

By passing an emptyFn option to connect.session(), which should return true if the session is considered empty, here's a way to avoid maintaining empty sessions and the corresponding set-cookie in responses.

This is needed for reverse-proxy caching where a set-cookie in a response defeats cache-ability. Also nice for performance reasons, i.e. a lightweight JSON endpoint that shouldn't be creating a session every time it's hit.

@jonathanong

i wouldn't call this "lazy sessions" as to me that means retrieving the session data only when requested by the app. Instead, this is "avoiding empty sessions", which is an optimization.

i instead would send a header from my reverse proxy (assuming i have control over it) and check the header:

app.use(function (req, res, next) {
  // Assume to be stateless. You should validate this.
  if (req.headers['x-api-key']) next();
  // Some random custom header to avoid session.
  else if (req.headers['x-stateless']) next();
  // Otherwise, proceed as normal.
  else connect.session(options)(req, res, next);
})
@jonathanong

i'll reopen this. i was thrown off by the "lazy sessions" and "reverse-proxy caching" keywords when it should just really say "don't create unmodified sessions".

however, for your use case, i would still agree that doing a check like i mentioned above is better (at least for now).

a real "lazy sessions" would be much better using getters and setters.

@jonathanong jonathanong reopened this
@jonathanong

also, next time you should refer to another issue it's solving. this seems to be what tj wants in #451

@tj tj was assigned
@jonathanong

development has moved to https://github.com/expressjs/session. i've added you to the team, so feel free to make necessary changes

@JoeWagner JoeWagner referenced this pull request in expressjs/session
Closed

Only set cookie and save new sessions if modified #45

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jun 7, 2013
  1. @carlos8f
This page is out of date. Refresh to see the latest.
Showing with 147 additions and 25 deletions.
  1. +66 −23 lib/middleware/session.js
  2. +81 −2 test/session.js
View
89 lib/middleware/session.js
@@ -191,6 +191,7 @@ function session(options){
, store = options.store || new MemoryStore
, cookie = options.cookie || {}
, trustProxy = options.proxy
+ , emptyFn = options.emptyFn || function (session) { return false }
, storeReady = true;
// notify user that this store is not
@@ -244,6 +245,19 @@ function session(options){
unsignedCookie = utils.parseSignedCookie(rawCookie, secret);
}
+ // lazy sessions flag
+ var isEmpty;
+
+ function sendCookie () {
+ var val = '';
+ if (!isEmpty) {
+ val = 's:' + signature.sign(req.sessionID, secret);
+ }
+ val = req.session.cookie.serialize(key, val);
+ debug('set-cookie %s', val);
+ res.setHeader('Set-Cookie', val);
+ }
+
// set-cookie
res.on('header', function(){
if (!req.session) return;
@@ -253,24 +267,36 @@ function session(options){
, secured = cookie.secure && tls
, isNew = unsignedCookie != req.sessionID;
- // only send secure cookies via https
- if (cookie.secure && !secured) return debug('not secured');
-
- // long expires, handle expiry server-side
- if (!isNew && cookie.hasLongExpires) return debug('already set cookie');
-
- // browser-session length cookie
- if (null == cookie.expires) {
- if (!isNew) return debug('already set browser-session cookie');
- // compare hashes and ids
- } else if (originalHash == hash(req.session) && originalId == req.session.id) {
- return debug('unmodified session');
+ isEmpty = emptyFn(req.session);
+ if (isEmpty) {
+ if (isNew) {
+ debug('new session is empty, deleting!');
+ // a session starts out empty, avoid setting cookie.
+ return;
+ }
+ else {
+ debug('deleting session that has become empty!');
+ // a session has become empty, try to expire the cookie.
+ cookie.expires = new Date(0);
+ }
+ }
+ else {
+ // only send secure cookies via https
+ if (cookie.secure && !secured) return debug('not secured');
+
+ // long expires, handle expiry server-side
+ if (!isNew && cookie.hasLongExpires) return debug('already set cookie');
+
+ // browser-session length cookie
+ if (null == cookie.expires) {
+ if (!isNew) return debug('already set browser-session cookie');
+ // compare hashes and ids
+ } else if (originalHash == hash(req.session) && originalId == req.session.id) {
+ return debug('unmodified session');
+ }
}
- var val = 's:' + signature.sign(req.sessionID, secret);
- val = cookie.serialize(key, val);
- debug('set-cookie %s', val);
- res.setHeader('Set-Cookie', val);
+ sendCookie();
});
// proxy end() to commit the session
@@ -278,13 +304,30 @@ function session(options){
res.end = function(data, encoding){
res.end = end;
if (!req.session) return res.end(data, encoding);
- debug('saving');
- req.session.resetMaxAge();
- req.session.save(function(err){
- if (err) console.error(err.stack);
- debug('saved');
- res.end(data, encoding);
- });
+ var isNew = unsignedCookie != req.sessionID;
+ if (typeof isEmpty === 'undefined') isEmpty = emptyFn(req.session);
+ if (isEmpty) {
+ // don't save new empty session
+ if (isNew) return res.end(data, encoding);
+ // destroy old empty session
+ if (!res._emittedHeader) {
+ req.session.cookie.expires = new Date(0);
+ sendCookie();
+ }
+ req.session.destroy(function (err) {
+ if (err) console.error(err.stack);
+ res.end(data, encoding);
+ });
+ }
+ else {
+ debug('saving');
+ req.session.resetMaxAge();
+ req.session.save(function(err){
+ if (err) console.error(err.stack);
+ debug('saved');
+ res.end(data, encoding);
+ });
+ }
};
// generate the session
View
83 test/session.js
@@ -10,8 +10,12 @@ function respond(req, res) {
function sid(res) {
var val = res.headers['set-cookie'];
- if (!val) return '';
- return /^connect\.sid=([^;]+);/.exec(val[0])[1];
+ try {
+ return /^connect\.sid=([^;]+);/.exec(val[0])[1];
+ }
+ catch (e) {
+ return '';
+ }
}
function expires(res) {
@@ -542,4 +546,79 @@ describe('connect.session()', function(){
})
})
+
+ describe('lazy sessions', function () {
+ function emptyFn (session) {
+ return session.feeling === 'blue';
+ }
+
+ it('should not create empty session', function (done) {
+ var app = connect()
+ .use(connect.cookieParser())
+ .use(connect.session({ secret: 'keyboard cat', cookie: { maxAge: min }, emptyFn: emptyFn }))
+ .use(function(req, res, next){
+ req.session.feeling = 'blue';
+ res.end();
+ });
+
+ app.request()
+ .get('/')
+ .end(function(res){
+ res.headers.should.not.have.property('set-cookie');
+ done();
+ });
+ })
+
+ var id;
+
+ function testLazySessions (writeHead) {
+ return function (done) {
+ var app = connect()
+ .use(connect.cookieParser())
+ .use(connect.session({ secret: 'keyboard cat', cookie: { maxAge: min }, emptyFn: emptyFn }))
+ .use(function(req, res, next){
+ req.session.feeling = 'good';
+ req.session.counter || (req.session.counter = 0);
+ req.session.counter++;
+ // on the third time, session becomes empty
+ if (req.session.counter === 3) req.session.feeling = 'blue';
+ if (writeHead) {
+ res.writeHead(200, {'Content-Type': 'text/plain'});
+ res.end('ok');
+ }
+ else {
+ res.end();
+ }
+ });
+
+ app.request()
+ .get('/')
+ .end(function(res){
+ res.headers.should.have.property('set-cookie');
+ id = sid(res);
+
+ app.request()
+ .get('/')
+ .set('Cookie', 'connect.sid=' + id)
+ .end(function(res){
+ res.headers.should.have.property('set-cookie');
+ sid(res).should.equal(id);
+
+ app.request()
+ .get('/')
+ .set('Cookie', 'connect.sid=' + id)
+ .end(function(res){
+ res.headers.should.have.property('set-cookie');
+ sid(res).should.equal('');
+ expires(res).should.equal('Thu, 01 Jan 1970 00:00:00 GMT');
+ done();
+ });
+ });
+ });
+ };
+ }
+
+ it('should maintain non-empty session (no writeHead())', testLazySessions(false));
+ it('should maintain non-empty session (with writeHead())', testLazySessions(true));
+ })
})
Something went wrong with that request. Please try again.