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

Polyfill the fetch Headers object if needed #1014

Merged
merged 2 commits into from
Sep 13, 2022
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -1524,7 +1524,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 @@ -1575,6 +1575,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