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

Allow valid accept headers in document loader options #182

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 35 additions & 15 deletions js/jsonld.js
Original file line number Diff line number Diff line change
@@ -1650,35 +1650,55 @@ jsonld.cache = {
*/
var _defaults = {
headers: {
accept: 'application/ld+json, application/json'
accept: ['application/ld+json', 'application/json']
}
};

/**
* Build an headers object from custom headers and assert `accept` header isn't overridden.
* Build an headers object from custom headers and assert `accept` header is valid.
*
* @param {Object} optionsHeaders an object (map) of headers
* with key as header name and value as header value.
* @return {Object} an object (map) of headers with a valid `accept` header.
*/
function buildHeaders(optionsHeaders) {
jsonld.buildHeaders = function(optionsHeaders) {
optionsHeaders = optionsHeaders || {};

var hasAccept = Object.keys(optionsHeaders).some(function(h) {
var headers = {};
var acceptHeaderKeys = Object.keys(optionsHeaders).filter(function(h) {
return h.toLowerCase() === 'accept';
});

if(hasAccept) {
throw new RangeError(
'Accept header may not be specified as an option; only "' +
_defaults.headers.accept + '" is supported.');
if(acceptHeaderKeys.length === 1) { // only one `accept` header key may be ok
// check if `accept` header is valid
var acceptHeaderValues = optionsHeaders[acceptHeaderKeys[0]]
.split(',')
.map(function(value) {
return value.trim().toLowerCase();
});

var acceptHeaderIsValid = _defaults.headers.accept.every(function(requiredAcceptHeaderValue) {
return acceptHeaderValues.indexOf(requiredAcceptHeaderValue) !== -1;
});

if(!acceptHeaderIsValid) {
throw new RangeError('Accept header must contains "' +
_defaults.headers.accept.join(', ') + '".');
}

headers['Accept'] = acceptHeaderValues.join(', ');
} else if(acceptHeaderKeys.length >= 2) { // there are too many accept headers
throw new RangeError('Accept header may be specified only once.');
} else { // no accept headers take the default values
headers['Accept'] = _defaults.headers.accept.join(', ');
}

var headers = {'Accept': _defaults.headers.accept};
for(var k in optionsHeaders) { headers[k] = optionsHeaders[k]; }
for(var k in optionsHeaders) {
if(k.toLowerCase() !== 'accept') { headers[k] = optionsHeaders[k]; }
}

return headers;
}
};

/**
* Document loaders.
@@ -1702,7 +1722,7 @@ jsonld.documentLoaders = {};
jsonld.documentLoaders.jquery = function($, options) {
options = options || {};
var queue = new jsonld.RequestQueue();
var headers = buildHeaders(options.headers);
var headers = jsonld.buildHeaders(options.headers);

// use option or, by default, use Promise when its defined
var usePromise = ('usePromise' in options ?
@@ -1732,7 +1752,7 @@ jsonld.documentLoaders.jquery = function($, options) {
$.ajax({
url: url,
accepts: {
json: _defaults.headers.accept
json: _defaults.headers.accept.join(', ')
},
headers: headers,
dataType: 'json',
@@ -1791,7 +1811,7 @@ jsonld.documentLoaders.jquery = function($, options) {
*/
jsonld.documentLoaders.node = function(options) {
options = options || {};
var headers = buildHeaders(options.headers);
var headers = jsonld.buildHeaders(options.headers);
var strictSSL = ('strictSSL' in options) ? options.strictSSL : true;
var maxRedirects = ('maxRedirects' in options) ? options.maxRedirects : -1;
var request = ('request' in options) ? options.request : require('request');
@@ -1934,7 +1954,7 @@ jsonld.documentLoaders.xhr = function(options) {
options = options || {};
var rlink = /(^|(\r\n))link:/i;
var queue = new jsonld.RequestQueue();
var headers = buildHeaders(options.headers);
var headers = jsonld.buildHeaders(options.headers);

// use option or, by default, use Promise when its defined
var usePromise = ('usePromise' in options ?
103 changes: 103 additions & 0 deletions test/buildHeaders.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
var jsonld = require('../js/jsonld');
var assert = require('assert');

describe('Build a valid header object from pre-defined headers', function() {

describe('When built with 2 accept headers', function() {
var headers = {
'Accept': 'application/json',
'accept': 'application/ld+json, application/json'
};


it('should fail', function() {
assert.throws(
jsonld.buildHeaders.bind(null, headers),
function(err) {
assert.ok(err instanceof RangeError, 'A range error should be thrown');
assert.equal(err.message, 'Accept header may be specified only once.');

return true;
}
);
});
});

describe('When built with no explicit headers', function() {
var headers = {};
it(
'the "Accept" header should default to "application/ld+json, application/json"',
function() {
var actualHeaders = jsonld.buildHeaders(headers);
assert.deepEqual(actualHeaders, {
'Accept': 'application/ld+json, application/json'
});
}
);
});

describe('When built with custom headers', function() {
var headers = {
'Authorization': 'Bearer d783jkjaods9f87o83hj'
};
it('the custom headers should be preserved', function() {
var actualHeaders = jsonld.buildHeaders(headers);
assert.deepEqual(actualHeaders, {
'Authorization': 'Bearer d783jkjaods9f87o83hj',
'Accept': 'application/ld+json, application/json'
});
});
});

describe('When built with an accept header equal to the default accept header value', function() {
var headers = {
'Accept': 'application/ld+json, application/json'
};
it(
'the accept header should remain unchanged',
function() {
var actualHeaders = jsonld.buildHeaders(headers);
assert.deepEqual(actualHeaders, headers);
}
);
});

describe('When built with a valid accept headers with extra acceptations', function() {
var headers = {
'Accept': 'application/ld+json, application/json, text/html'
};
it(
'the accept header should remain unchanged',
function() {
var actualHeaders = jsonld.buildHeaders(headers);
assert.deepEqual(actualHeaders, headers);
}
);
});

describe('When built with an invalid accept header', function() {
var possibleInvalidHeaders = [
{ 'Accept': 'text/html' },
{ 'Accept': 'application/ld+json, application/jsonbourne' },
{ 'Accept': 'application/ld+json application/json' }
];

for (var i in possibleInvalidHeaders) {
console.log(possibleInvalidHeaders[i]);
it('should fail', function() {
assert.throws(
jsonld.buildHeaders.bind(null, possibleInvalidHeaders[i]),
function(err) {
assert.ok(err instanceof RangeError, 'A range error should be thrown');
assert.equal(
err.message,
'Accept header must contains "application/ld+json, application/json".'
);

return true;
}
);
});
}
});
});
4 changes: 2 additions & 2 deletions test/node-document-loader-tests.js
Original file line number Diff line number Diff line change
@@ -78,15 +78,15 @@ describe('For the node.js document loader', function() {
});
});

describe('When built using headers that already contain an Accept header', function() {
describe('When built using headers that already contain an invalid Accept header', function() {
var options = {request: requestMock};
options.headers = {
'x-test-header-3': 'Third value',
'Accept': 'video/mp4'
};

it('constructing the document loader should fail', function() {
var expectedMessage = 'Accept header may not be specified as an option; only "application/ld+json, application/json" is supported.';
var expectedMessage = 'Accept header must contains "application/ld+json, application/json".';
assert.throws(
jsonld.useDocumentLoader.bind(jsonld, documentLoaderType, options),
function(err) {