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

Carlos Rodriguez jongleberry TJ Holowaychuk
Carlos Rodriguez

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.

jongleberry

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);
})
jongleberry

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.

jongleberry jonathanong reopened this
jongleberry

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

TJ Holowaychuk tj was assigned
jongleberry

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

Joe Wagner 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. Carlos Rodriguez
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
89 lib/middleware/session.js
View
@@ -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
83 test/session.js
View
@@ -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.