Skip to content

Commit

Permalink
Polyfill the fetch Headers object if needed (#1014)
Browse files Browse the repository at this point in the history
* test: improve test description

* fix: polyfill the fetch Headers object if needed
  • Loading branch information
waltjones authored Sep 13, 2022
1 parent c2c5310 commit acf2dfe
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 2 deletions.
3 changes: 2 additions & 1 deletion src/browser/telemetry.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
var _ = require('../utility');
var headers = require('../utility/headers');
var scrub = require('../scrub');
var urlparser = require('./url');
var domUtil = require('./domUtility');
Expand Down Expand Up @@ -362,7 +363,7 @@ Instrumenter.prototype.instrumentNetwork = function() {
if (args[1] && args[1].headers) {
// Argument may be a Headers object, or plain object. Ensure here that
// we are working with a Headers object with case-insensitive keys.
var reqHeaders = new Headers(args[1].headers);
var reqHeaders = headers(args[1].headers);

metadata.request_content_type = reqHeaders.get('Content-Type');

Expand Down
94 changes: 94 additions & 0 deletions src/utility/headers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* headers - Detect when fetch Headers are undefined and use a partial polyfill.
*
* A full polyfill is not used in order to keep package size as small as possible.
* Since this is only used internally and is not added to the window object,
* the full interface doesn't need to be supported.
*
* This implementation is modified from whatwg-fetch:
* https://github.com/github/fetch
*/
function headers(headers) {
if (typeof Headers === 'undefined') {
return new FetchHeaders(headers);
}

return new Headers(headers);
}

function normalizeName(name) {
if (typeof name !== 'string') {
name = String(name)
}
return name.toLowerCase()
}

function normalizeValue(value) {
if (typeof value !== 'string') {
value = String(value)
}
return value
}

function iteratorFor(items) {
var iterator = {
next: function() {
var value = items.shift()
return {done: value === undefined, value: value}
}
}

return iterator
}

function FetchHeaders(headers) {
this.map = {}

if (headers instanceof FetchHeaders) {
headers.forEach(function(value, name) {
this.append(name, value)
}, this)
} else if (Array.isArray(headers)) {
headers.forEach(function(header) {
this.append(header[0], header[1])
}, this)
} else if (headers) {
Object.getOwnPropertyNames(headers).forEach(function(name) {
this.append(name, headers[name])
}, this)
}
}

FetchHeaders.prototype.append = function(name, value) {
name = normalizeName(name)
value = normalizeValue(value)
var oldValue = this.map[name]
this.map[name] = oldValue ? oldValue + ', ' + value : value
}

FetchHeaders.prototype.get = function(name) {
name = normalizeName(name)
return this.has(name) ? this.map[name] : null
}

FetchHeaders.prototype.has = function(name) {
return this.map.hasOwnProperty(normalizeName(name))
}

FetchHeaders.prototype.forEach = function(callback, thisArg) {
for (var name in this.map) {
if (this.map.hasOwnProperty(name)) {
callback.call(thisArg, this.map[name], name, this)
}
}
}

FetchHeaders.prototype.entries = function() {
var items = []
this.forEach(function(value, name) {
items.push([name, value])
})
return iteratorFor(items)
}

module.exports = headers;
70 changes: 69 additions & 1 deletion test/browser.rollbar.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1606,7 +1606,7 @@ describe('options.autoInstrument', function() {
})
});

it('should add telemetry events for fetch calls', function(done) {
it('should report error for http 4xx fetch calls, when enabled', function(done) {
var server = window.server;
stubResponse(server);
server.requests.length = 0;
Expand Down Expand Up @@ -1659,6 +1659,74 @@ describe('options.autoInstrument', function() {
})
});

it('should add telemetry headers when fetch Headers object is undefined', function(done) {
var server = window.server;
stubResponse(server);
server.requests.length = 0;

window.fetchStub = sinon.stub(window, 'fetch');

var readableStream = new ReadableStream({
start(controller) {
controller.enqueue(JSON.stringify({name: 'foo', password: '123456'}));
controller.close();
}
});

window.fetch.returns(Promise.resolve(new Response(
readableStream,
{ status: 200, statusText: 'OK', headers: { 'content-type': 'application/json', 'password': '123456' }}
)));

var options = {
accessToken: 'POST_CLIENT_ITEM_TOKEN',
autoInstrument: {
log: false,
network: true,
networkResponseHeaders: true,
networkRequestHeaders: true
}
};
var rollbar = window.rollbar = new Rollbar(options);

// Remove Headers from window object
var originalHeaders = window.Headers;
delete window.Headers;

const fetchInit = {
method: 'POST',
headers: {'Content-Type': 'application/json', Secret: '123456'},
body: JSON.stringify({name: 'bar', secret: 'xhr post'})
};
var fetchRequest = new Request('https://example.com/xhr-test');
window.fetch(fetchRequest, fetchInit)
.then(function(response) {
try {
rollbar.log('test'); // generate a payload to inspect
server.respond();

expect(server.requests.length).to.eql(1);
var body = JSON.parse(server.requests[0].requestBody);

// Verify request headers capture and case-insensitive scrubbing
expect(body.data.body.telemetry[0].body.request_headers).to.eql({'content-type': 'application/json', secret: '********'});

// Verify response headers capture and case-insensitive scrubbing
expect(body.data.body.telemetry[0].body.response.headers).to.eql({'content-type': 'application/json', password: '********'});

// Assert that the original stream reader hasn't been read.
expect(response.bodyUsed).to.eql(false);

rollbar.configure({ autoInstrument: false });
window.fetch.restore();
window.Headers = originalHeaders;
done();
} catch (e) {
done(e);
}
})
});

it('should add a diagnostic message when wrapConsole fails', function(done) {
var server = window.server;
stubResponse(server);
Expand Down

0 comments on commit acf2dfe

Please sign in to comment.