From 85038b7d6299bfd0adaf7c16d99af77be4a6adcf Mon Sep 17 00:00:00 2001 From: Walt Jones Date: Tue, 29 Nov 2022 13:24:50 -0500 Subject: [PATCH] enable compat with service workers and v3 extensions --- src/apiUtility.js | 16 ++- src/browser/transport.js | 195 +++++---------------------------- src/browser/transport/fetch.js | 35 ++++++ src/browser/transport/xhr.js | 159 +++++++++++++++++++++++++++ test/apiUtility.test.js | 54 +++++++++ test/browser.transport.test.js | 59 ++++++++++ 6 files changed, 350 insertions(+), 168 deletions(-) create mode 100644 src/browser/transport/fetch.js create mode 100644 src/browser/transport/xhr.js diff --git a/src/apiUtility.js b/src/apiUtility.js index 8f138f75..7c8aac78 100644 --- a/src/apiUtility.js +++ b/src/apiUtility.js @@ -25,6 +25,7 @@ function getTransportFromOptions(options, defaults, url) { var path = defaults.path; var search = defaults.search; var timeout = options.timeout; + var transport = detectTransport(options) var proxy = options.proxy; if (options.endpoint) { @@ -42,16 +43,26 @@ function getTransportFromOptions(options, defaults, url) { port: port, path: path, search: search, - proxy: proxy + proxy: proxy, + transport: transport }; } +function detectTransport(options) { + var gWindow = ((typeof window != 'undefined') && window) || ((typeof self != 'undefined') && self); + var transport = options.defaultTransport || 'xhr'; + if (typeof gWindow.fetch === 'undefined') transport = 'xhr'; + if (typeof gWindow.XMLHttpRequest === 'undefined') transport = 'fetch'; + return transport; +} + function transportOptions(transport, method) { var protocol = transport.protocol || 'https:'; var port = transport.port || (protocol === 'http:' ? 80 : protocol === 'https:' ? 443 : undefined); var hostname = transport.hostname; var path = transport.path; var timeout = transport.timeout; + var transportAPI = transport.transport; if (transport.search) { path = path + transport.search; } @@ -67,7 +78,8 @@ function transportOptions(transport, method) { hostname: hostname, path: path, port: port, - method: method + method: method, + transport: transportAPI }; } diff --git a/src/browser/transport.js b/src/browser/transport.js index a79ef622..968342af 100644 --- a/src/browser/transport.js +++ b/src/browser/transport.js @@ -1,7 +1,6 @@ -/*global XDomainRequest*/ - var _ = require('../utility'); -var logger = require('./logger'); +var makeFetchRequest = require('./transport/fetch'); +var makeXhrRequest = require('./transport/xhr'); /* * accessToken may be embedded in payload but that should not @@ -13,6 +12,7 @@ var logger = require('./logger'); * path * port * method + * transport ('xhr' | 'fetch') * } * * params is an object containing key/value pairs. These @@ -32,7 +32,9 @@ Transport.prototype.get = function(accessToken, options, params, callback, reque var method = 'GET'; var url = _.formatUrl(options); - _makeZoneRequest(accessToken, url, method, null, callback, requestFactory, options.timeout); + this._makeZoneRequest( + accessToken, url, method, null, callback, requestFactory, options.timeout, options.transport + ); } Transport.prototype.post = function(accessToken, options, payload, callback, requestFactory) { @@ -57,7 +59,9 @@ Transport.prototype.post = function(accessToken, options, payload, callback, req var writeData = stringifyResult.value; var method = 'POST'; var url = _.formatUrl(options); - _makeZoneRequest(accessToken, url, method, writeData, callback, requestFactory, options.timeout); + this._makeZoneRequest( + accessToken, url, method, writeData, callback, requestFactory, options.timeout, options.transport + ); } Transport.prototype.postJsonPayload = function (accessToken, options, jsonPayload, callback, requestFactory) { @@ -67,7 +71,9 @@ Transport.prototype.postJsonPayload = function (accessToken, options, jsonPayloa var method = 'POST'; var url = _.formatUrl(options); - _makeZoneRequest(accessToken, url, method, jsonPayload, callback, requestFactory, options.timeout); + this._makeZoneRequest( + accessToken, url, method, jsonPayload, callback, requestFactory, options.timeout, options.transport + ); } @@ -75,7 +81,7 @@ Transport.prototype.postJsonPayload = function (accessToken, options, jsonPayloa // so Angular change detection isn't triggered on each API call. // This is the equivalent of runOutsideAngular(). // -function _makeZoneRequest() { +Transport.prototype._makeZoneRequest = function () { var gWindow = ((typeof window != 'undefined') && window) || ((typeof self != 'undefined') && self); var currentZone = gWindow && gWindow.Zone && gWindow.Zone.current; var args = Array.prototype.slice.call(arguments); @@ -83,10 +89,24 @@ function _makeZoneRequest() { if (currentZone && currentZone._name === 'angular') { var rootZone = currentZone._parent; rootZone.run(function () { - _makeRequest.apply(undefined, args); + this._makeRequest.apply(undefined, args); }); } else { - _makeRequest.apply(undefined, args); + this._makeRequest.apply(undefined, args); + } +} + +Transport.prototype._makeRequest = function ( + accessToken, url, method, data, callback, requestFactory, timeout, transport +) { + if (typeof RollbarProxy !== 'undefined') { + return _proxyRequest(data, callback); + } + + if (transport === 'fetch') { + makeFetchRequest(accessToken, url, method, data, callback, timeout) + } else { + makeXhrRequest(accessToken, url, method, data, callback, requestFactory, timeout) } } @@ -102,161 +122,4 @@ function _proxyRequest(json, callback) { ); } -function _makeRequest(accessToken, url, method, data, callback, requestFactory, timeout) { - if (typeof RollbarProxy !== 'undefined') { - return _proxyRequest(data, callback); - } - - var request; - if (requestFactory) { - request = requestFactory(); - } else { - request = _createXMLHTTPObject(); - } - if (!request) { - // Give up, no way to send requests - return callback(new Error('No way to send a request')); - } - try { - try { - var onreadystatechange = function() { - try { - if (onreadystatechange && request.readyState === 4) { - onreadystatechange = undefined; - - var parseResponse = _.jsonParse(request.responseText); - if (_isSuccess(request)) { - callback(parseResponse.error, parseResponse.value); - return; - } else if (_isNormalFailure(request)) { - if (request.status === 403) { - // likely caused by using a server access token - var message = parseResponse.value && parseResponse.value.message; - logger.error(message); - } - // return valid http status codes - callback(new Error(String(request.status))); - } else { - // IE will return a status 12000+ on some sort of connection failure, - // so we return a blank error - // http://msdn.microsoft.com/en-us/library/aa383770%28VS.85%29.aspx - var msg = 'XHR response had no status code (likely connection failure)'; - callback(_newRetriableError(msg)); - } - } - } catch (ex) { - //jquery source mentions firefox may error out while accessing the - //request members if there is a network error - //https://github.com/jquery/jquery/blob/a938d7b1282fc0e5c52502c225ae8f0cef219f0a/src/ajax/xhr.js#L111 - var exc; - if (ex && ex.stack) { - exc = ex; - } else { - exc = new Error(ex); - } - callback(exc); - } - }; - - request.open(method, url, true); - if (request.setRequestHeader) { - request.setRequestHeader('Content-Type', 'application/json'); - request.setRequestHeader('X-Rollbar-Access-Token', accessToken); - } - - if(_.isFiniteNumber(timeout)) { - request.timeout = timeout; - } - - request.onreadystatechange = onreadystatechange; - request.send(data); - } catch (e1) { - // Sending using the normal xmlhttprequest object didn't work, try XDomainRequest - if (typeof XDomainRequest !== 'undefined') { - - // Assume we are in a really old browser which has a bunch of limitations: - // http://blogs.msdn.com/b/ieinternals/archive/2010/05/13/xdomainrequest-restrictions-limitations-and-workarounds.aspx - - // Extreme paranoia: if we have XDomainRequest then we have a window, but just in case - if (!window || !window.location) { - return callback(new Error('No window available during request, unknown environment')); - } - - // If the current page is http, try and send over http - if (window.location.href.substring(0, 5) === 'http:' && url.substring(0, 5) === 'https') { - url = 'http' + url.substring(5); - } - - var xdomainrequest = new XDomainRequest(); - xdomainrequest.onprogress = function() {}; - xdomainrequest.ontimeout = function() { - var msg = 'Request timed out'; - var code = 'ETIMEDOUT'; - callback(_newRetriableError(msg, code)); - }; - xdomainrequest.onerror = function() { - callback(new Error('Error during request')); - }; - xdomainrequest.onload = function() { - var parseResponse = _.jsonParse(xdomainrequest.responseText); - callback(parseResponse.error, parseResponse.value); - }; - xdomainrequest.open(method, url, true); - xdomainrequest.send(data); - } else { - callback(new Error('Cannot find a method to transport a request')); - } - } - } catch (e2) { - callback(e2); - } -} - -function _createXMLHTTPObject() { - /* global ActiveXObject:false */ - - var factories = [ - function () { - return new XMLHttpRequest(); - }, - function () { - return new ActiveXObject('Msxml2.XMLHTTP'); - }, - function () { - return new ActiveXObject('Msxml3.XMLHTTP'); - }, - function () { - return new ActiveXObject('Microsoft.XMLHTTP'); - } - ]; - var xmlhttp; - var i; - var numFactories = factories.length; - for (i = 0; i < numFactories; i++) { - /* eslint-disable no-empty */ - try { - xmlhttp = factories[i](); - break; - } catch (e) { - // pass - } - /* eslint-enable no-empty */ - } - return xmlhttp; -} - -function _isSuccess(r) { - return r && r.status && r.status === 200; -} - -function _isNormalFailure(r) { - return r && _.isType(r.status, 'number') && r.status >= 400 && r.status < 600; -} - -function _newRetriableError(message, code) { - var err = new Error(message); - err.code = code || 'ENOTFOUND'; - return err; -} - module.exports = Transport; diff --git a/src/browser/transport/fetch.js b/src/browser/transport/fetch.js new file mode 100644 index 00000000..35c817a9 --- /dev/null +++ b/src/browser/transport/fetch.js @@ -0,0 +1,35 @@ +var logger = require('../logger'); +var _ = require('../../utility'); + +function makeFetchRequest(accessToken, url, method, data, callback, timeout) { + var controller; + var timeoutId; + + if(_.isFiniteNumber(timeout)) { + controller = new AbortController(); + timeoutId = setTimeout(() => controller.abort(), timeout); + } + + fetch(url, { + method: method, + headers: { + 'Content-Type': 'application/json', + 'X-Rollbar-Access-Token': accessToken, + signal: controller.signal + }, + body: data, + }) + .then((response) => { + if (timeoutId) clearTimeout(timeoutId); + return response.json(); + }) + .then((data) => { + callback(null, data); + }) + .catch((error) => { + logger.error(error.message); + callback(error); + }); +} + +module.exports = makeFetchRequest; diff --git a/src/browser/transport/xhr.js b/src/browser/transport/xhr.js new file mode 100644 index 00000000..b14d4529 --- /dev/null +++ b/src/browser/transport/xhr.js @@ -0,0 +1,159 @@ +/*global XDomainRequest*/ + +var _ = require('../../utility'); +var logger = require('../logger'); + +function makeXhrRequest(accessToken, url, method, data, callback, requestFactory, timeout) { + var request; + if (requestFactory) { + request = requestFactory(); + } else { + request = _createXMLHTTPObject(); + } + if (!request) { + // Give up, no way to send requests + return callback(new Error('No way to send a request')); + } + try { + try { + var onreadystatechange = function() { + try { + if (onreadystatechange && request.readyState === 4) { + onreadystatechange = undefined; + + var parseResponse = _.jsonParse(request.responseText); + if (_isSuccess(request)) { + callback(parseResponse.error, parseResponse.value); + return; + } else if (_isNormalFailure(request)) { + if (request.status === 403) { + // likely caused by using a server access token + var message = parseResponse.value && parseResponse.value.message; + logger.error(message); + } + // return valid http status codes + callback(new Error(String(request.status))); + } else { + // IE will return a status 12000+ on some sort of connection failure, + // so we return a blank error + // http://msdn.microsoft.com/en-us/library/aa383770%28VS.85%29.aspx + var msg = 'XHR response had no status code (likely connection failure)'; + callback(_newRetriableError(msg)); + } + } + } catch (ex) { + //jquery source mentions firefox may error out while accessing the + //request members if there is a network error + //https://github.com/jquery/jquery/blob/a938d7b1282fc0e5c52502c225ae8f0cef219f0a/src/ajax/xhr.js#L111 + var exc; + if (ex && ex.stack) { + exc = ex; + } else { + exc = new Error(ex); + } + callback(exc); + } + }; + + request.open(method, url, true); + if (request.setRequestHeader) { + request.setRequestHeader('Content-Type', 'application/json'); + request.setRequestHeader('X-Rollbar-Access-Token', accessToken); + } + + if(_.isFiniteNumber(timeout)) { + request.timeout = timeout; + } + + request.onreadystatechange = onreadystatechange; + request.send(data); + } catch (e1) { + // Sending using the normal xmlhttprequest object didn't work, try XDomainRequest + if (typeof XDomainRequest !== 'undefined') { + + // Assume we are in a really old browser which has a bunch of limitations: + // http://blogs.msdn.com/b/ieinternals/archive/2010/05/13/xdomainrequest-restrictions-limitations-and-workarounds.aspx + + // Extreme paranoia: if we have XDomainRequest then we have a window, but just in case + if (!window || !window.location) { + return callback(new Error('No window available during request, unknown environment')); + } + + // If the current page is http, try and send over http + if (window.location.href.substring(0, 5) === 'http:' && url.substring(0, 5) === 'https') { + url = 'http' + url.substring(5); + } + + var xdomainrequest = new XDomainRequest(); + xdomainrequest.onprogress = function() {}; + xdomainrequest.ontimeout = function() { + var msg = 'Request timed out'; + var code = 'ETIMEDOUT'; + callback(_newRetriableError(msg, code)); + }; + xdomainrequest.onerror = function() { + callback(new Error('Error during request')); + }; + xdomainrequest.onload = function() { + var parseResponse = _.jsonParse(xdomainrequest.responseText); + callback(parseResponse.error, parseResponse.value); + }; + xdomainrequest.open(method, url, true); + xdomainrequest.send(data); + } else { + callback(new Error('Cannot find a method to transport a request')); + } + } + } catch (e2) { + callback(e2); + } +} + +function _createXMLHTTPObject() { + /* global ActiveXObject:false */ + + var factories = [ + function () { + return new XMLHttpRequest(); + }, + function () { + return new ActiveXObject('Msxml2.XMLHTTP'); + }, + function () { + return new ActiveXObject('Msxml3.XMLHTTP'); + }, + function () { + return new ActiveXObject('Microsoft.XMLHTTP'); + } + ]; + var xmlhttp; + var i; + var numFactories = factories.length; + for (i = 0; i < numFactories; i++) { + /* eslint-disable no-empty */ + try { + xmlhttp = factories[i](); + break; + } catch (e) { + // pass + } + /* eslint-enable no-empty */ + } + return xmlhttp; +} + +function _isSuccess(r) { + return r && r.status && r.status === 200; +} + +function _isNormalFailure(r) { + return r && _.isType(r.status, 'number') && r.status >= 400 && r.status < 600; +} + +function _newRetriableError(message, code) { + var err = new Error(message); + err.code = code || 'ENOTFOUND'; + return err; +} + +module.exports = makeXhrRequest; diff --git a/test/apiUtility.test.js b/test/apiUtility.test.js index 66748a97..ca440ec6 100644 --- a/test/apiUtility.test.js +++ b/test/apiUtility.test.js @@ -105,6 +105,60 @@ describe('getTransportFromOptions', function() { expect(t.proxy).to.eql(options.proxy); expect(t.timeout).to.eql(undefined); }); + describe('getTransportFromOptions', function() { + var defaults = { + hostname: 'api.com', + protocol: 'https:', + path: '/api/1', + search: '?abc=456', + }; + var url = { + parse: function(_) { + return { + hostname: 'whatever.com', + protocol: 'http:', + pathname: '/api/42' + }; + } + }; + it('should use xhr by default', function(done) { + var options = {}; + var t = u.getTransportFromOptions(options, defaults, url); + expect(t.transport).to.eql('xhr'); + done(); + }); + it('should use fetch when requested', function(done) { + var options = {defaultTransport: 'fetch'}; + var t = u.getTransportFromOptions(options, defaults, url); + expect(t.transport).to.eql('fetch'); + done(); + }); + it('should use xhr when requested', function(done) { + var options = {defaultTransport: 'xhr'}; + var t = u.getTransportFromOptions(options, defaults, url); + expect(t.transport).to.eql('xhr'); + done(); + }); + it('should use xhr when fetch is unavailable', function(done) { + var options = {defaultTransport: 'fetch'}; + var oldFetch = window.fetch; + self.fetch = undefined; + var t = u.getTransportFromOptions(options, defaults, url); + expect(t.transport).to.eql('xhr'); + self.fetch = oldFetch; + done(); + }); + it('should use fetch when xhr is unavailable', function(done) { + var options = {defaultTransport: 'xhr'}; + var oldXhr = window.XMLHttpRequest; + self.XMLHttpRequest = undefined; + var t = u.getTransportFromOptions(options, defaults, url); + expect(t.transport).to.eql('fetch'); + self.XMLHttpRequest = oldXhr; + done(); + }); + }); + }); describe('transportOptions', function() { diff --git a/test/browser.transport.test.js b/test/browser.transport.test.js index 66cb1018..9f50d9cd 100644 --- a/test/browser.transport.test.js +++ b/test/browser.transport.test.js @@ -86,6 +86,65 @@ describe('post', function() { }; t.post(accessToken, options, payload, callback, requestFactory.getInstance); }); + describe('post', function() { + beforeEach(function (done) { + window.fetchStub = sinon.stub(window, 'fetch'); + window.server = sinon.createFakeServer(); + done(); + }); + + afterEach(function () { + window.fetch.restore(); + window.server.restore(); + }); + + function stubFetchResponse() { + window.fetch.returns(Promise.resolve(new Response( + JSON.stringify({ err: 0, message: 'OK', result: { uuid: uuid }}), + { status: 200, statusText: 'OK', headers: { 'Content-Type': 'application/json' }} + ))); + } + + function stubXhrResponse() { + window.server.respondWith( + [ + 200, + { 'Content-Type': 'application/json' }, + '{"err": 0, "result":{ "uuid": "d4c7acef55bf4c9ea95e4fe9428a8287"}}' + ] + ); + } + + var uuid = 'd4c7acef55bf4c9ea95e4fe9428a8287'; + + it('should use fetch when requested', function(done) { + var callback = function(err, resp) { + expect(window.fetchStub.called).to.be.ok(); + expect(server.requests.length).to.eql(0); + done(err); + }; + stubFetchResponse(); + stubXhrResponse(); + server.requests.length = 0; + options.transport = 'fetch'; + t.post(accessToken, options, payload, callback); + }); + it('should use xhr when requested', function(done) { + var callback = function(err, resp) { + expect(window.fetchStub.called).to.not.be.ok(); + expect(server.requests.length).to.eql(1); + done(err); + }; + stubFetchResponse(); + stubXhrResponse(); + server.requests.length = 0; + options.transport = 'xhr'; + t.post(accessToken, options, payload, callback); + setTimeout(function() { + server.respond(); + }, 1); + }); + }); }); var TestRequest = function(response, status, shouldThrowOnSend) {