Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support both deflate and gzip #15

Merged
merged 4 commits into from Mar 10, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
35 changes: 25 additions & 10 deletions index.js
Expand Up @@ -38,7 +38,6 @@ function Versions(options) {

// Read in the various of configurations that we want to merge in to our own
// configuration object.
//
if (!options.cloned) {
this.read('../../node_modules/package.json'); // For version number
this.read('./versions.json'); // For our defaults
Expand Down Expand Up @@ -298,16 +297,21 @@ Versions.prototype.initialize = function initialize(type) {
*/
Versions.prototype.write = function write(req, res, data) {
var age = this.get('max age')
, body = data.buffer;
, body = data.buffer
, type;

// Check if we have a GZIP version of the content.
if ('gzip' in data) {
if (this.allows('gzip', req)) {
res.setHeader('Content-Encoding', 'gzip');
body = data.gzip;
this.metrics.incr('gzip');
if ('compressed' in data) {
// Force GZIP over deflate as it is more stable.
if (this.allows('deflate', req) && data.compressed.deflate) type = 'deflate';
if (this.allows('gzip', req) && data.compressed.gzip) type = 'gzip';

if (type) {
res.setHeader('Content-Encoding', type);
body = data.compressed[type];
this.metrics.incr(type);
} else {
this.metrics.incr('gzip blocked');
this.metrics.incr('compression blocked');
}
}

Expand All @@ -330,7 +334,7 @@ Versions.prototype.write = function write(req, res, data) {
* @returns {Boolean}
* @api public
*/
Versions.prototype.allows = function supports(what, req) {
Versions.prototype.allows = function allows(what, req) {
var headers = req.headers;

switch (what) {
Expand Down Expand Up @@ -361,6 +365,9 @@ Versions.prototype.allows = function supports(what, req) {

return obfuscated;

case 'deflate':
return !!~(headers['accept-encoding'] || '').toLowerCase().indexOf('deflate');

// Do we allow this extension to be served from our server?
case 'extension':
req.extension = req.extension || path.extname(req.url);
Expand Down Expand Up @@ -391,9 +398,17 @@ Versions.prototype.allows = function supports(what, req) {
* @api private
*/
Versions.prototype.compress = function compress(type, data, callback) {
var compressed = Object.create(null);

function iterator(error, content, method) {
compressed[method] = !error ? content : null;
if (Object.keys(compressed).length === 2) callback(null, compressed);
}

// Only these types of content should be gzipped.
if (/json|text|javascript|xml/i.test(type || '') || type in this.compressTypes) {
zlib.gzip(data, callback);
zlib.gzip(data, function (error, content) { iterator(error, content, 'gzip'); });
zlib.deflate(data, function (error, content) { iterator(error, content, 'deflate'); });
} else {
process.nextTick(callback);
}
Expand Down
2 changes: 1 addition & 1 deletion lib/pull.js
Expand Up @@ -65,7 +65,7 @@ module.exports = function pull(req, res, next) {
// Compress the data and pass in the origin response so our compression
// function can check if this resource needs to be compressed
self.compress(data['content-type'], body, function compiling(err, compressed) {
if (compressed && !err) data.gzip = compressed;
if (compressed && !err) data.compressed = compressed;

res.setHeader('X-Cache', 'Pull');
self.write(req, res, data);
Expand Down
4 changes: 3 additions & 1 deletion package.json
Expand Up @@ -40,6 +40,8 @@
"devDependencies": {
"chai": "1.5.x",
"mocha": "1.8.x",
"redis": "0.8.x"
"redis": "0.8.x",
"sinon": "1.6.x",
"sinon-chai": "2.3.x"
}
}
94 changes: 88 additions & 6 deletions test/server.test.js
Expand Up @@ -3,8 +3,11 @@ describe('versions()', function () {

var chai = require('chai')
, path = require('path')
, sinon = require('sinon')
, sinonChai = require('sinon-chai')
, expect = chai.expect;

chai.use(sinonChai);
chai.Assertion.includeStack = true;

describe('initialization', function () {
Expand Down Expand Up @@ -235,18 +238,87 @@ describe('versions()', function () {
});

describe('#write', function () {
var versions;
var versions, req, res, data;

before(function () {
versions = require('../').clone();
versions.logger.notification = 8;
versions.initialize('server');
});

beforeEach(function () {
res = {
setHeader: sinon.stub()
, end: sinon.stub()
};

data = {
buffer: 'test'
, 'content-type': 'application/json'
, 'last-modified': 1234
, compressed: {
deflate: 'body with deflated content'
, gzip: 'body with gzipped content'
}
};
});

after(function (done) {
versions.end(done);
});

it('sets the correct headers');
it('decided which content to use');
it('sets the correct headers', function () {
var age = 86400000
, exp = new Date(Date.now() + age).toUTCString();
req = { headers: { 'accept-encoding': 'cakes' } };
versions.set('max age', age).write(req, res, data);

expect(res.setHeader.callCount).to.be.equal(5);
expect(res.setHeader).to.be.calledWith('Expires', exp);
expect(res.setHeader).to.be.calledWith('Cache-Control', 'max-age='+ age +', public');
expect(res.setHeader).to.be.calledWith('Last-Modified', data['last-modified']);
expect(res.setHeader).to.be.calledWith('Content-Type', data['content-type']);
expect(res.setHeader).to.be.calledWith('Content-Length', data.buffer.length);
});

it('returns uncompressed buffer if non requested', function () {
req = { headers: { 'accept-encoding': 'cakes' } };
versions.write(req, res, data);

expect(res.setHeader.callCount).to.be.equal(5);
expect(res.end).to.be.calledWith('test');
expect(res.end).to.be.calledOnce;
});

it('returns uncompressed buffer no compression available', function () {
req = { headers: { 'accept-encoding': 'gzip,deflate' } };
data = { buffer: 'test' };
versions.write(req, res, data);

expect(res.setHeader.callCount).to.be.equal(5);
expect(res.end).to.be.calledWith('test');
expect(res.end).to.be.calledOnce;
});

it('prefers gzip over deflate', function () {
req = { headers: { 'accept-encoding': 'gzip,deflate' } };
versions.write(req, res, data);

expect(res.setHeader).to.be.calledWith('Content-Encoding', 'gzip');
expect(res.setHeader.callCount).to.be.equal(6);
expect(res.end).to.be.calledWith('body with gzipped content');
expect(res.end).to.be.calledOnce;
});

it('returns compression type deflate if requested', function () {
req = { headers: { 'accept-encoding': 'deflate' } };
versions.write(req, res, data);

expect(res.setHeader).to.be.calledWith('Content-Encoding', 'deflate');
expect(res.end).to.be.calledWith('body with deflated content');
expect(res.setHeader.callCount).to.be.equal(6);
expect(res.end).to.be.calledOnce;
});
});

describe('#allows', function () {
Expand Down Expand Up @@ -280,6 +352,11 @@ describe('versions()', function () {
expect(versions.allows('gzip', decline)).to.equal(false);
});

it('detects deflate support', function () {
expect(versions.allows('deflate', accept)).to.equal(true);
expect(versions.allows('deflate', decline)).to.equal(false);
});

it('ignores IE6 without service pack', function () {
accept.headers['user-agent'] = 'Mozilla/5.0 (compatible; MSIE 6.0; Windows NT 5.1)';

Expand Down Expand Up @@ -374,9 +451,14 @@ describe('versions()', function () {

versions.compress('text/javascript', buffer, function (err, data) {
expect(err).to.equal(null);
expect(Buffer.isBuffer(data)).to.equal(true);
expect(data.toString()).to.not.equal(buffer.toString());
expect(data.length).to.be.below(buffer.length);
expect(data).to.have.property('gzip');
expect(data).to.have.property('deflate');

Object.keys(data).forEach(function type (key) {
expect(Buffer.isBuffer(data[key])).to.equal(true);
expect(data[key].toString()).to.not.equal(buffer.toString());
expect(data[key].length).to.be.below(buffer.length);
});

done();
});
Expand Down