diff --git a/lib/manager.js b/lib/manager.js index e3e91b557a..2b0b755df2 100644 --- a/lib/manager.js +++ b/lib/manager.js @@ -79,6 +79,7 @@ function Manager (server, options) { , 'browser client cache': true , 'browser client minification': false , 'browser client etag': false + , 'browser client expires': 315360000 , 'browser client gzip': false , 'browser client handler': false , 'client store expiration': 15 diff --git a/lib/static.js b/lib/static.js index 03505ddd40..e3117eda84 100644 --- a/lib/static.js +++ b/lib/static.js @@ -42,7 +42,8 @@ var mime = { * @api private */ -var bundle = /\+((?:\+)?[\w\-]+)*(?:\.js)$/g; +var bundle = /\+((?:\+)?[\w\-]+)*(?:\.v\d+\.\d+\.\d+)?(?:\.js)$/ + , versioning = /\.v\d+\.\d+\.\d+(?:\.js)$/; /** * Export the constructor @@ -120,10 +121,14 @@ Static.prototype.init = function () { build(self.manager.get('transports'), callback); }); + this.add('/socket.io.v', { mime: mime.js }, function (path, callback) { + build(self.manager.get('transports'), callback); + }); + // allow custom builds based on url paths this.add('/socket.io+', { mime: mime.js }, function (path, callback) { var available = self.manager.get('transports') - , matches = bundle.exec(path) + , matches = path.match(bundle) , transports = []; if (!matches) return callback('No valid transports'); @@ -217,7 +222,7 @@ Static.prototype.has = function (path) { , i = keys.length; while (i--) { - if (!!~path.indexOf(keys[i])) return this.paths[keys[i]]; + if (-~path.indexOf(keys[i])) return this.paths[keys[i]]; } return false; @@ -271,7 +276,13 @@ Static.prototype.write = function (path, req, res) { function write (status, headers, content, encoding) { try { res.writeHead(status, headers || undefined); - res.end(content || '', encoding || undefined); + + // only write content if it's not a HEAD request and we actually have + // some content to write (304's doesn't have content). + res.end( + req.method !== 'HEAD' && content ? content : '' + , encoding || undefined + ); } catch (e) {} } @@ -291,19 +302,28 @@ Static.prototype.write = function (path, req, res) { var accept = req.headers['accept-encoding'] || '' , gzip = !!~accept.toLowerCase().indexOf('gzip') , mime = reply.mime + , versioned = reply.versioned , headers = { 'Content-Type': mime.type }; // check if we can add a etag - if (self.manager.enabled('browser client etag') && reply.etag) { + if (self.manager.enabled('browser client etag') && reply.etag && !versioned) { headers['Etag'] = reply.etag; } - // check if we can send gzip data + // see if we need to set Expire headers because the path is versioned + if (versioned) { + var expires = self.manager.get('browser client expires'); + headers['Cache-Control'] = 'private, x-gzip-ok="", max-age=' + expires; + headers['Date'] = new Date().toUTCString(); + headers['Expires'] = new Date(Date.now() + (expires * 1000)).toUTCString(); + } + if (gzip && reply.gzip) { headers['Content-Length'] = reply.gzip.length; headers['Content-Encoding'] = 'gzip'; + headers['Vary'] = 'Accept-Encoding'; write(200, headers, reply.gzip.content, mime.encoding); } else { headers['Content-Length'] = reply.length; @@ -342,6 +362,7 @@ Static.prototype.write = function (path, req, res) { , length: content.length , mime: details.mime , etag: etag || client.version + , versioned: versioning.test(path) }; // check if gzip is enabled diff --git a/test/common.js b/test/common.js index 1b5b9d3198..2030bede51 100644 --- a/test/common.js +++ b/test/common.js @@ -147,6 +147,24 @@ HTTPClient.prototype.post = function (path, data, opts, fn) { return this.request(path, opts, fn); }; +/** + * Issue a HEAD request + * + * @api private + */ + +HTTPClient.prototype.head = function (path, opts, fn) { + if ('function' == typeof opts) { + fn = opts; + opts = {}; + } + + opts = opts || {}; + opts.method = 'HEAD'; + + return this.request(path, opts, fn); +}; + /** * Performs a handshake (GET) request * diff --git a/test/static.test.js b/test/static.test.js index 06ca98e05b..6feed80b58 100644 --- a/test/static.test.js +++ b/test/static.test.js @@ -24,7 +24,9 @@ module.exports = { , io = sio.listen(port); (!!io.static.has('/socket.io.js')).should.be.true; - (!!io.static.has('/socket.io+')).should.be.true; + (!!io.static.has('/socket.io.v1.0.0.js')).should.be.true; + (!!io.static.has('/socket.io+xhr-polling.js')).should.be.true; + (!!io.static.has('/socket.io+xhr-polling.v1.0.0.js')).should.be.true; (!!io.static.has('/static/flashsocket/WebSocketMain.swf')).should.be.true; (!!io.static.has('/static/flashsocket/WebSocketMainInsecure.swf')).should.be.true; @@ -455,6 +457,67 @@ module.exports = { io.server.close(); done(); }); - } + }, + + 'test that HEAD requests work': function (done) { + var port = ++ports + , io = sio.listen(port) + , cl = client(port); + + cl.head('/socket.io/socket.io.js', function (res, data) { + res.headers['content-type'].should.eql('application/javascript'); + res.headers['content-length'].should.match(/([0-9]+)/); + + data.should.eql(''); + + cl.end(); + io.server.close() + done(); + }); + }, + + 'test that a versioned client is served': function (done) { + var port = ++ports + , io = sio.listen(port) + , cl = client(port); + + cl.get('/socket.io/socket.io.v0.8.9.js', function (res, data) { + res.headers['content-type'].should.eql('application/javascript'); + res.headers['content-length'].should.match(/([0-9]+)/); + res.headers['cache-control'] + .indexOf(io.get('browser client expires')).should.be.above(-1); + + data.should.match(/XMLHttpRequest/); + + cl.end(); + io.server.close(); + done(); + }); + }, + + 'test that a custom versioned build client is served': function (done) { + var port = ++ports + , io = sio.listen(port) + , cl = client(port); + + io.set('browser client expires', 1337); + + cl.get('/socket.io/socket.io+websocket.v0.8.10.js', function (res, data) { + res.headers['content-type'].should.eql('application/javascript'); + res.headers['content-length'].should.match(/([0-9]+)/); + res.headers['cache-control'] + .indexOf(io.get('browser client expires')).should.be.above(-1); + data.should.match(/XMLHttpRequest/); + data.should.match(/WS\.prototype\.name/); + data.should.not.match(/Flashsocket\.prototype\.name/); + data.should.not.match(/HTMLFile\.prototype\.name/); + data.should.not.match(/JSONPPolling\.prototype\.name/); + data.should.not.match(/XHRPolling\.prototype\.name/); + + cl.end(); + io.server.close(); + done(); + }); + } };