Skip to content

Commit

Permalink
Allow configuring how the set of preferred encodings is sorted.
Browse files Browse the repository at this point in the history
Options:
client: use client quality levels and client order for equal quality levels
clientThenServer: use client quality levels and server order for equal quality levels
server: use server order

The 'client' preferred order is the default to match with previous implementation.

Signed-off-by: Jonas Berlin <[email protected]>
  • Loading branch information
Mikko Tiihonen committed Aug 26, 2016
1 parent 751c381 commit 2ac5a24
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 15 deletions.
5 changes: 5 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
unreleased
==========

* Add configurable preferred sorting order for `Accept-Encoding`

0.6.1 / 2016-05-02
==================

Expand Down
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,19 +168,29 @@ You can check a working example at `examples/encoding.js`.

Returns the most preferred encoding from the client.

##### encoding(availableEncodings)
##### encoding(availableEncodings, options)

Returns the most preferred encoding from a list of available encodings.

##### encodings()

Returns an array of preferred encodings ordered by the client preference.

##### encodings(availableEncodings)
##### encodings(availableEncodings, options)

Returns an array of preferred encodings ordered by priority from a list of
available encodings.

The options has one available option. The default value if not specified is
```js
{ sortPreference: 'client' }
```

#### Sort options
`client`: sorts first by client quality level and then by client given order
`clientThenServer`: sorts first by client quality level and then by server given order
`server`: sorts by server given order

## See Also

The [accepts](https://npmjs.org/package/accepts#readme) module builds on
Expand Down
8 changes: 4 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ Negotiator.prototype.charsets = function charsets(available) {
return preferredCharsets(this.request.headers['accept-charset'], available);
};

Negotiator.prototype.encoding = function encoding(available) {
var set = this.encodings(available);
Negotiator.prototype.encoding = function encoding(available, options) {
var set = this.encodings(available, options);
return set && set[0];
};

Negotiator.prototype.encodings = function encodings(available) {
Negotiator.prototype.encodings = function encodings(available, options) {
var preferredEncodings = loadModule('encoding').preferredEncodings;
return preferredEncodings(this.request.headers['accept-encoding'], available);
return preferredEncodings(this.request.headers['accept-encoding'], available, options);
};

Negotiator.prototype.language = function language(available) {
Expand Down
41 changes: 33 additions & 8 deletions lib/encoding.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ module.exports.preferredEncodings = preferredEncodings;
*/

var simpleEncodingRegExp = /^\s*([^\s;]+)\s*(?:;(.*))?$/;
var comparators = {
client: compareSpecsPreferClient,
clientThenServer: compareSpecsPreferClientThenServer,
server: compareSpecsPreferServer
};

/**
* Parse the Accept-Encoding header.
Expand Down Expand Up @@ -50,7 +55,7 @@ function parseAcceptEncoding(accept) {
*/
accepts[j++] = {
encoding: 'identity',
q: minQuality,
q: minQuality * 0.1,
i: i
};
}
Expand All @@ -74,8 +79,8 @@ function parseEncoding(str, i) {
var q = 1;
if (match[2]) {
var params = match[2].split(';');
for (var i = 0; i < params.length; i ++) {
var p = params[i].trim().split('=');
for (var j = 0; j < params.length; j ++) {
var p = params[j].trim().split('=');
if (p[0] === 'q') {
q = parseFloat(p[1]);
break;
Expand Down Expand Up @@ -135,23 +140,25 @@ function specify(encoding, spec, index) {
* @public
*/

function preferredEncodings(accept, provided) {
function preferredEncodings(accept, provided, options) {
var accepts = parseAcceptEncoding(accept || '');

if (!provided) {
// sorted list of all encodings
return accepts
.filter(isQuality)
.sort(compareSpecs)
.sort(compareSpecsPreferClient)
.map(getFullEncoding);
}

var priorities = provided.map(function getPriority(type, index) {
return getEncodingPriority(type, accepts, index);
});

var comparator = options && comparators[options.sortPreference] || compareSpecsPreferClient

// sorted list of accepted encodings
return priorities.filter(isQuality).sort(compareSpecs).map(function getEncoding(priority) {
return priorities.filter(isQuality).sort(comparator).map(function getEncoding(priority) {
return provided[priorities.indexOf(priority)];
});
}
Expand All @@ -161,8 +168,26 @@ function preferredEncodings(accept, provided) {
* @private
*/

function compareSpecs(a, b) {
return (b.q - a.q) || (b.s - a.s) || (a.o - b.o) || (a.i - b.i) || 0;
function compareSpecsPreferClient(a, b) {
return (b.q - a.q) || (b.s - a.s) || (a.o - b.o) || 0;
}

/**
* Compare two specs.
* @private
*/

function compareSpecsPreferClientThenServer(a, b) {
return (b.q - a.q) || (a.i - b.i) || 0;
}

/**
* Compare two specs, ignoring client quality levels.
* @private
*/

function compareSpecsPreferServer(a, b) {
return (a.i - b.i) || 0;
}

/**
Expand Down
121 changes: 120 additions & 1 deletion test/encoding.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
var assert = require('assert')
var Negotiator = require('..')

var clientPreference = { sortPreference: 'client'}
var serverPreference = { sortPreference: 'server'}
var clientThenServerPreference = { sortPreference: 'clientThenServer'}

describe('negotiator.encoding()', function () {
whenAcceptEncoding(undefined, function () {
it('should return identity', function () {
Expand Down Expand Up @@ -112,6 +116,12 @@ describe('negotiator.encoding(array)', function () {
assert.strictEqual(this.negotiator.encoding(['gzip']), 'gzip')
assert.strictEqual(this.negotiator.encoding(['compress', 'gzip']), 'gzip')
})

it('clientThenServerPreference: should return server-preferred encoding', function () {
assert.strictEqual(this.negotiator.encoding(['identity'], clientThenServerPreference), 'identity')
assert.strictEqual(this.negotiator.encoding(['gzip'], clientThenServerPreference), 'gzip')
assert.strictEqual(this.negotiator.encoding(['compress', 'gzip'], clientThenServerPreference), 'compress')
})
})

whenAcceptEncoding('*, gzip;q=0', function () {
Expand Down Expand Up @@ -206,6 +216,23 @@ describe('negotiator.encoding(array)', function () {
assert.strictEqual(this.negotiator.encoding(['compress', 'identity']), 'identity')
})
})

whenAcceptEncoding('gzip;q=0.9, sdhc, br;q=0.9', function () {
it('should return best server-preferred encoding of equal client-preferred encodings', function () {
assert.strictEqual(this.negotiator.encoding(['compress', 'br', 'gzip']), 'gzip')
assert.strictEqual(this.negotiator.encoding(['compress', 'gzip', 'br']), 'gzip')
})

it('should return best server-preferred encoding of equal client-preferred encodings', function () {
assert.strictEqual(this.negotiator.encoding(['compress', 'br', 'gzip'], clientThenServerPreference), 'br')
assert.strictEqual(this.negotiator.encoding(['compress', 'gzip', 'br'], clientThenServerPreference), 'gzip')
})

it('should return best server-preferred encoding of equal client-preferred encodings', function () {
assert.strictEqual(this.negotiator.encoding(['compress', 'br', 'gzip'], serverPreference), 'br')
assert.strictEqual(this.negotiator.encoding(['compress', 'gzip', 'br'], serverPreference), 'gzip')
})
})
})

describe('negotiator.encodings()', function () {
Expand Down Expand Up @@ -316,7 +343,13 @@ describe('negotiator.encodings(array)', function () {
it('should prefer gzip', function () {
assert.deepEqual(this.negotiator.encodings(['identity']), ['identity'])
assert.deepEqual(this.negotiator.encodings(['gzip']), ['gzip'])
assert.deepEqual(this.negotiator.encodings(['compress', 'gzip']), ['gzip', 'compress'])
assert.deepEqual(this.negotiator.encodings(['compress', 'deflate', 'gzip']), ['gzip', 'compress', 'deflate'])
})

it('clientThenServerPreference: should return server preferred encodings', function () {
assert.deepEqual(this.negotiator.encodings(['identity'], clientThenServerPreference), ['identity'])
assert.deepEqual(this.negotiator.encodings(['gzip'], clientThenServerPreference), ['gzip'])
assert.deepEqual(this.negotiator.encodings(['compress', 'deflate', 'gzip'], clientThenServerPreference), ['compress', 'deflate', 'gzip'])
})
})

Expand Down Expand Up @@ -403,6 +436,11 @@ describe('negotiator.encodings(array)', function () {
assert.deepEqual(this.negotiator.encodings(['deflate']), ['deflate'])
assert.deepEqual(this.negotiator.encodings(['deflate', 'gzip']), ['deflate', 'gzip'])
})

it('serverPreference: should ignore client quality levels', function () {
assert.deepEqual(this.negotiator.encodings(['deflate', 'gzip'], serverPreference), ['deflate', 'gzip'])
assert.deepEqual(this.negotiator.encodings(['gzip', 'deflate'], serverPreference), ['gzip', 'deflate'])
})
})

whenAcceptEncoding('gzip;q=0.8, identity;q=0.5, *;q=0.3', function () {
Expand All @@ -411,6 +449,87 @@ describe('negotiator.encodings(array)', function () {
assert.deepEqual(this.negotiator.encodings(['identity', 'gzip', 'compress']), ['gzip', 'identity', 'compress'])
})
})

whenAcceptEncoding('not;q=0, med1;q=0.9, med2;q=0.9, high, *;q=0.8', function () {
it('clientPreference', function () {
assert.deepEqual(this.negotiator.encodings(['high', 'not', 'med2', 'other', 'med1'], clientPreference), ['high', 'med1', 'med2', 'other'])
assert.deepEqual(this.negotiator.encodings(['high', 'not', 'med1', 'other', 'med2'], clientPreference), ['high', 'med1', 'med2', 'other'])
})

it('clientThenServerPreference', function () {
assert.deepEqual(this.negotiator.encodings(['high', 'not', 'med2', 'other', 'med1'], clientThenServerPreference), ['high', 'med2', 'med1', 'other'])
assert.deepEqual(this.negotiator.encodings(['high', 'not', 'med1', 'other', 'med2'], clientThenServerPreference), ['high', 'med1', 'med2', 'other'])
})

it('serverPreference', function () {
assert.deepEqual(this.negotiator.encodings(['high', 'not', 'med2', 'other', 'med1'], serverPreference), ['high', 'med2', 'other', 'med1'])
assert.deepEqual(this.negotiator.encodings(['high', 'not', 'med1', 'other', 'med2'], serverPreference), ['high', 'med1', 'other', 'med2'])
})
})

whenAcceptEncoding('not;q=0, med1;q=0.9, med2;q=0.9, high, identity;q=0.9', function () {
it('clientPreference', function () {
assert.deepEqual(this.negotiator.encodings(['high', 'not', 'med2', 'other', 'med1', 'identity'], clientPreference), ['high', 'med1', 'med2', 'identity'])
assert.deepEqual(this.negotiator.encodings(['high', 'not', 'med1', 'other', 'med2', 'identity'], clientPreference), ['high', 'med1', 'med2', 'identity'])
})

it('clientThenServerPreference', function () {
assert.deepEqual(this.negotiator.encodings(['high', 'not', 'med2', 'other', 'med1', 'identity'], clientThenServerPreference), ['high', 'med2', 'med1', 'identity'])
assert.deepEqual(this.negotiator.encodings(['high', 'not', 'med1', 'other', 'med2', 'identity'], clientThenServerPreference), ['high', 'med1', 'med2', 'identity'])
})

it('serverPreference', function () {
assert.deepEqual(this.negotiator.encodings(['high', 'not', 'med2', 'other', 'med1', 'identity'], serverPreference), ['high', 'med2', 'med1', 'identity'])
assert.deepEqual(this.negotiator.encodings(['high', 'not', 'med1', 'other', 'med2', 'identity'], serverPreference), ['high', 'med1', 'med2', 'identity'])
})
})

whenAcceptEncoding('c;q=0.9, b;q=0.89, a;q=0.9, d;q=0.91, e;q=0.9', function () {
it('clientPreference', function () {
assert.deepEqual(this.negotiator.encodings(['b','c','d','e'], clientPreference), ['d', 'c', 'e', 'b']);
assert.deepEqual(this.negotiator.encodings(['c','d','e','b'], clientPreference), ['d', 'c', 'e', 'b']);
assert.deepEqual(this.negotiator.encodings(['d','e','b','c'], clientPreference), ['d', 'c', 'e', 'b']);
assert.deepEqual(this.negotiator.encodings(['e','b','c','d'], clientPreference), ['d', 'c', 'e', 'b']);

assert.deepEqual(this.negotiator.encodings(['a','c','e'], clientPreference), ['c', 'a', 'e']);
assert.deepEqual(this.negotiator.encodings(['c','e','a'], clientPreference), ['c', 'a', 'e']);
assert.deepEqual(this.negotiator.encodings(['e','a','c'], clientPreference), ['c', 'a', 'e']);

assert.deepEqual(this.negotiator.encodings(['identity','e','f'], clientPreference), ['e','identity']);
assert.deepEqual(this.negotiator.encodings(['f','identity','e'], clientPreference), ['e','identity']);
assert.deepEqual(this.negotiator.encodings(['e','f','identity'], clientPreference), ['e','identity']);
})

it('clientThenServerPreference', function () {
assert.deepEqual(this.negotiator.encodings(['b','c','d','e'], clientThenServerPreference), ['d', 'c', 'e', 'b']);
assert.deepEqual(this.negotiator.encodings(['c','d','e','b'], clientThenServerPreference), ['d', 'c', 'e', 'b']);
assert.deepEqual(this.negotiator.encodings(['d','e','b','c'], clientThenServerPreference), ['d', 'e', 'c', 'b']);
assert.deepEqual(this.negotiator.encodings(['e','b','c','d'], clientThenServerPreference), ['d', 'e', 'c', 'b']);

assert.deepEqual(this.negotiator.encodings(['a','c','e'], clientThenServerPreference), ['a', 'c', 'e']);
assert.deepEqual(this.negotiator.encodings(['c','e','a'], clientThenServerPreference), ['c', 'e', 'a']);
assert.deepEqual(this.negotiator.encodings(['e','a','c'], clientThenServerPreference), ['e', 'a', 'c']);

assert.deepEqual(this.negotiator.encodings(['identity','e','f'], clientThenServerPreference), ['e','identity']);
assert.deepEqual(this.negotiator.encodings(['f','identity','e'], clientThenServerPreference), ['e','identity']);
assert.deepEqual(this.negotiator.encodings(['e','f','identity'], clientThenServerPreference), ['e','identity']);
})

it('serverPreference', function () {
assert.deepEqual(this.negotiator.encodings(['b','c','d','e'], serverPreference), ['b','c','d','e']);
assert.deepEqual(this.negotiator.encodings(['c','d','e','b'], serverPreference), ['c','d','e','b']);
assert.deepEqual(this.negotiator.encodings(['d','e','b','c'], serverPreference), ['d','e','b','c']);
assert.deepEqual(this.negotiator.encodings(['e','b','c','d'], serverPreference), ['e','b','c','d']);

assert.deepEqual(this.negotiator.encodings(['a','c','e'], serverPreference), ['a', 'c', 'e']);
assert.deepEqual(this.negotiator.encodings(['c','e','a'], serverPreference), ['c', 'e', 'a']);
assert.deepEqual(this.negotiator.encodings(['e','a','c'], serverPreference), ['e', 'a', 'c']);

assert.deepEqual(this.negotiator.encodings(['identity','e','f'], serverPreference), ['identity', 'e']);
assert.deepEqual(this.negotiator.encodings(['f','identity','e'], serverPreference), ['identity', 'e']);
assert.deepEqual(this.negotiator.encodings(['e','f','identity'], serverPreference), ['e','identity']);
})
})
})

function createRequest(headers) {
Expand Down

0 comments on commit 2ac5a24

Please sign in to comment.