From 46d62441c302bd632531bac809331e92c7b2050b Mon Sep 17 00:00:00 2001 From: LavrovArtem Date: Tue, 29 Aug 2017 17:17:08 +0300 Subject: [PATCH] fix `The onreadystatechange property of XMLHttpRequest.prototype redefined by Angular` (close #1283) --- src/client/sandbox/xhr.js | 74 ++++++++++-------------- test/client/fixtures/sandbox/xhr-test.js | 31 +++++++++- 2 files changed, 61 insertions(+), 44 deletions(-) diff --git a/src/client/sandbox/xhr.js b/src/client/sandbox/xhr.js index ca67a1bb91..d56dda3ea1 100644 --- a/src/client/sandbox/xhr.js +++ b/src/client/sandbox/xhr.js @@ -54,8 +54,16 @@ export default class XhrSandbox extends SandboxBase { attach (window) { super.attach(window); - const xhrSandbox = this; - const xmlHttpRequestProto = window.XMLHttpRequest.prototype; + const xhrSandbox = this; + const xmlHttpRequestProto = window.XMLHttpRequest.prototype; + const xhrConstructorString = nativeMethods.XMLHttpRequest.toString(); + + const emitXhrCompletedEvent = function () { + if (this.readyState === this.DONE) { + xhrSandbox.emit(xhrSandbox.XHR_COMPLETED_EVENT, { xhr: this }); + nativeMethods.xhrRemoveEventListener.call(this, 'readystatechange', emitXhrCompletedEvent); + } + }; const syncCookieWithClient = function () { if (this.readyState < this.HEADERS_RECEIVED) @@ -73,6 +81,20 @@ export default class XhrSandbox extends SandboxBase { nativeMethods.xhrRemoveEventListener.call(this, 'readystatechange', syncCookieWithClient); }; + const xhrConstructorWrapper = function () { + const xhr = new nativeMethods.XMLHttpRequest(); + + nativeMethods.xhrAddEventListener.call(xhr, 'readystatechange', emitXhrCompletedEvent); + nativeMethods.xhrAddEventListener.call(xhr, 'readystatechange', syncCookieWithClient); + + return xhr; + }; + + window.XMLHttpRequest = xhrConstructorWrapper; + xhrConstructorWrapper.prototype = xmlHttpRequestProto; + xhrConstructorWrapper.toString = () => xhrConstructorString; + xmlHttpRequestProto.constructor = xhrConstructorWrapper; + xmlHttpRequestProto.abort = function () { nativeMethods.xhrAbort.apply(this, arguments); xhrSandbox.emit(xhrSandbox.XHR_ERROR_EVENT, { @@ -94,7 +116,6 @@ export default class XhrSandbox extends SandboxBase { if (typeof arguments[1] === 'string') arguments[1] = getProxyUrl(arguments[1]); - nativeMethods.xhrAddEventListener.call(this, 'readystatechange', syncCookieWithClient); nativeMethods.xhrOpen.apply(this, arguments); }; @@ -103,55 +124,24 @@ export default class XhrSandbox extends SandboxBase { xhrSandbox.emit(xhrSandbox.BEFORE_XHR_SEND_EVENT, { xhr }); - const orscHandler = () => { - if (this.readyState === 4) - xhrSandbox.emit(xhrSandbox.XHR_COMPLETED_EVENT, { xhr }); - }; - - // NOTE: If we're using the sync mode or if the response is in cache, - // we need to raise the callback manually. - if (this.readyState === 4) - orscHandler(); - else { - // NOTE: Get out of the current execution tick and then proxy onreadystatechange, - // because jQuery assigns a handler after the send() method was called. - nativeMethods.setTimeout.call(xhrSandbox.window, () => { - // NOTE: If the state is already changed, we just call the handler without proxying - // onreadystatechange. - if (this.readyState === 4) - orscHandler(); - - else if (typeof this.onreadystatechange === 'function') { - const originalHandler = this.onreadystatechange; - - this.onreadystatechange = progress => { - orscHandler(); - originalHandler.call(this, progress); - }; - } - else - this.addEventListener('readystatechange', orscHandler, false); - }, 0); - } - // NOTE: Add the XHR request mark, so that a proxy can recognize a request as a XHR request. As all // requests are passed to the proxy, we need to perform Same Origin Policy compliance checks on the // server side. So, we pass the CORS support flag to inform the proxy that it can analyze the // Access-Control_Allow_Origin flag and skip "preflight" requests. - nativeMethods.xhrSetRequestHeader.call(this, XHR_HEADERS.requestMarker, 'true'); - - nativeMethods.xhrSetRequestHeader.call(this, XHR_HEADERS.origin, getOriginHeader()); + nativeMethods.xhrSetRequestHeader.call(xhr, XHR_HEADERS.requestMarker, 'true'); + nativeMethods.xhrSetRequestHeader.call(xhr, XHR_HEADERS.origin, getOriginHeader()); if (xhrSandbox.corsSupported) - nativeMethods.xhrSetRequestHeader.call(this, XHR_HEADERS.corsSupported, 'true'); + nativeMethods.xhrSetRequestHeader.call(xhr, XHR_HEADERS.corsSupported, 'true'); - if (this.withCredentials) - nativeMethods.xhrSetRequestHeader.call(this, XHR_HEADERS.withCredentials, 'true'); + if (xhr.withCredentials) + nativeMethods.xhrSetRequestHeader.call(xhr, XHR_HEADERS.withCredentials, 'true'); - nativeMethods.xhrSend.apply(this, arguments); + nativeMethods.xhrSend.apply(xhr, arguments); // NOTE: For xhr with the sync mode - syncCookieWithClient.call(this); + emitXhrCompletedEvent.call(xhr); + syncCookieWithClient.call(xhr); }; xmlHttpRequestProto.getResponseHeader = function (name) { diff --git a/test/client/fixtures/sandbox/xhr-test.js b/test/client/fixtures/sandbox/xhr-test.js index 5ac199c0cf..6f06808fac 100644 --- a/test/client/fixtures/sandbox/xhr-test.js +++ b/test/client/fixtures/sandbox/xhr-test.js @@ -7,6 +7,7 @@ var settings = hammerhead.get('./settings'); var iframeSandbox = hammerhead.sandbox.iframe; var browserUtils = hammerhead.utils.browser; var nativeMethods = hammerhead.nativeMethods; +var xhrSandbox = hammerhead.sandbox.xhr; QUnit.testStart(function () { iframeSandbox.on(iframeSandbox.RUN_TASK_SCRIPT_EVENT, initIframeTestHandler); @@ -43,13 +44,16 @@ test('redirect requests to proxy', function () { }); test('createNativeXHR', function () { - window.XMLHttpRequest = function () {}; + var storedXMLHttpRequest = window.XMLHttpRequest; + + window.XMLHttpRequest = function () { + }; var xhr = XhrSandbox.createNativeXHR(); ok(xhr instanceof nativeMethods.XMLHttpRequest); - window.XMLHttpRequest = nativeMethods.XMLHttpRequest; + window.XMLHttpRequest = storedXMLHttpRequest; var isWrappedFunctionRE = /return 'function is wrapped'/; @@ -201,3 +205,26 @@ asyncTest('authorization headers by client should be processed (GH-1016)', funct }); xhr.send(); }); + +asyncTest('our internal the onreadystatechange handler must be first (GH-1283)', function () { + var xhr = new XMLHttpRequest(); + var timeout = null; + var testDone = function (eventObj) { + clearTimeout(timeout); + ok(!!eventObj); + start(); + }; + + var readyStateChangeHandler = function (e) { + if (this.readyState === this.DONE) + e.stopImmediatePropagation(); + }; + + xhr.addEventListener('readystatechange', readyStateChangeHandler, true); + xhr.onreadystatechange = readyStateChangeHandler; + xhrSandbox.on(xhrSandbox.XHR_COMPLETED_EVENT, testDone); + timeout = setTimeout(testDone, 2000); + + xhr.open('GET', '/xhr-test/', true); + xhr.send(); +});