Skip to content

Commit

Permalink
Fix issue with multiple requests in prevent-xhr
Browse files Browse the repository at this point in the history
  • Loading branch information
AdamWr committed Aug 1, 2023
1 parent 5c1ccff commit 7de58b6
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 3 deletions.
9 changes: 8 additions & 1 deletion CHANGELOG.md
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<!-- TODO: add @added tag to the files with specific version -->
<!-- during new scriptlets or redirects releasing -->

## [Unreleased]

### Fixed

- `prevent-xhr` closure bug on multiple requests
[#347](https://github.com/AdguardTeam/Scriptlets/issues/347)

## [v1.9.61] - 2023-08-01

### Added
Expand Down Expand Up @@ -219,7 +226,7 @@ prevent inline `onerror` and match `link` tag [#276](https://github.com/AdguardT
- `metrika-yandex-tag` [#254](https://github.com/AdguardTeam/Scriptlets/issues/254)
- `googlesyndication-adsbygoogle` [#252](https://github.com/AdguardTeam/Scriptlets/issues/252)


[Unreleased]: https://github.com/AdguardTeam/Scriptlets/compare/v1.9.58...HEAD
[v1.9.58]: https://github.com/AdguardTeam/Scriptlets/compare/v1.9.57...v1.9.58
[v1.9.57]: https://github.com/AdguardTeam/Scriptlets/compare/v1.9.37...v1.9.57
[v1.9.37]: https://github.com/AdguardTeam/Scriptlets/compare/v1.9.7...v1.9.37
Expand Down
15 changes: 13 additions & 2 deletions src/scriptlets/prevent-xhr.js
Expand Up @@ -112,6 +112,8 @@ export function preventXHR(source, propsToMatch, customResponseText) {

const nativeOpen = window.XMLHttpRequest.prototype.open;
const nativeSend = window.XMLHttpRequest.prototype.send;
const nativeGetResponseHeader = window.XMLHttpRequest.prototype.getResponseHeader;
const nativeGetAllResponseHeaders = window.XMLHttpRequest.prototype.getAllResponseHeaders;

let xhrData;
let modifiedResponse = '';
Expand All @@ -128,6 +130,9 @@ export function preventXHR(source, propsToMatch, customResponseText) {
hit(source);
} else if (matchRequestProps(source, propsToMatch, xhrData)) {
thisArg.shouldBePrevented = true;
// Add xhrData to thisArg to keep original values in case of multiple requests
// https://github.com/AdguardTeam/Scriptlets/issues/347
thisArg.xhrData = xhrData;
}

// Trap setRequestHeader of target xhr object to mimic request headers later;
Expand Down Expand Up @@ -194,7 +199,7 @@ export function preventXHR(source, propsToMatch, customResponseText) {
readyState: { value: readyState, writable: false },
statusText: { value: statusText, writable: false },
// If the request is blocked, responseURL is an empty string
responseURL: { value: responseURL || xhrData.url, writable: false },
responseURL: { value: responseURL || thisArg.xhrData.url, writable: false },
responseXML: { value: responseXML, writable: false },
// modified values
status: { value: 200, writable: false },
Expand All @@ -217,7 +222,7 @@ export function preventXHR(source, propsToMatch, customResponseText) {
hit(source);
});

nativeOpen.apply(forgedRequest, [xhrData.method, xhrData.url]);
nativeOpen.apply(forgedRequest, [thisArg.xhrData.method, thisArg.xhrData.url]);

// Mimic request headers before sending
// setRequestHeader can only be called on open request objects
Expand Down Expand Up @@ -246,6 +251,9 @@ export function preventXHR(source, propsToMatch, customResponseText) {
* @returns {string|null} Header value or null if header is not set.
*/
const getHeaderWrapper = (target, thisArg, args) => {
if (!thisArg.shouldBePrevented) {
return nativeGetResponseHeader.apply(thisArg, args);
}
if (!thisArg.collectedHeaders.length) {
return null;
}
Expand All @@ -270,6 +278,9 @@ export function preventXHR(source, propsToMatch, customResponseText) {
* @returns {string} All headers as a string. For no headers an empty string is returned.
*/
const getAllHeadersWrapper = (target, thisArg) => {
if (!thisArg.shouldBePrevented) {
return nativeGetAllResponseHeaders.call(thisArg);
}
if (!thisArg.collectedHeaders.length) {
return '';
}
Expand Down
52 changes: 52 additions & 0 deletions tests/scriptlets/prevent-xhr.test.js
Expand Up @@ -696,13 +696,15 @@ if (isSupported) {

xhr1.onload = () => {
assert.strictEqual(xhr1.readyState, 4, 'Response done');
assert.ok(xhr1.responseURL.includes(URL_TO_PASS.substring(1)), 'Origianl URL mocked');
assert.ok(xhr1.response, 'Response data exists');
assert.strictEqual(window.hit, undefined, 'hit should not fire');
done();
};

xhr2.onload = () => {
assert.strictEqual(xhr2.readyState, 4, 'Response done');
assert.ok(xhr2.responseURL.includes(URL_TO_BLOCK.substring(1)), 'Origianl URL mocked');
assert.strictEqual(typeof xhr2.responseText, 'string', 'Response text mocked');
assert.strictEqual(window.hit, 'FIRED', 'hit function fired');
clearGlobalProps('hit');
Expand All @@ -713,6 +715,56 @@ if (isSupported) {
// use timeout to avoid hit collisions
setTimeout(() => xhr2.send(), 10);
});

// https://github.com/AdguardTeam/Scriptlets/issues/347
test('Works correctly with different parallel XHR requests and blocked request', async (assert) => {
const METHOD = 'GET';
// advert.js does not exist, it imitate blocked request
const URL_TO_BLOCK = `${FETCH_OBJECTS_PATH}/advert.js`;
const URL_TO_PASS = `${FETCH_OBJECTS_PATH}/test01.json`;
const MATCH_DATA = ['advert.js'];

runScriptlet(name, MATCH_DATA);

const done = assert.async(2);

const xhr1 = new XMLHttpRequest();
const xhr2 = new XMLHttpRequest();

xhr1.open(METHOD, URL_TO_BLOCK);
xhr2.open(METHOD, URL_TO_PASS);

xhr1.onload = () => {
const responseHeader = xhr1.getResponseHeader('date');
const responseHeaders = xhr1.getAllResponseHeaders();

assert.strictEqual(xhr1.readyState, 4, 'Response done');
assert.ok(responseHeaders.length === 0, 'Response header is empty');
assert.strictEqual(responseHeader, null, 'Response header date returns null');
assert.strictEqual(typeof xhr1.responseText, 'string', 'Response text');
assert.ok(xhr1.responseText.length === 0, 'Response text is empty');
assert.ok(xhr1.responseURL.includes(URL_TO_BLOCK.substring(1)), 'Origianl URL mocked');
assert.strictEqual(window.hit, 'FIRED', 'hit function fired');
done();
};

xhr2.onload = () => {
const responseHeader = xhr2.getResponseHeader('date');
const responseHeaders = xhr2.getAllResponseHeaders();

assert.strictEqual(xhr2.readyState, 4, 'Response done');
assert.ok(responseHeaders.length > 0, 'Response header contains data');
assert.ok(responseHeader.length > 0, 'Response header date is not empty');
assert.ok(xhr2.responseURL.includes(URL_TO_PASS.substring(1)), 'Origianl URL mocked');
assert.strictEqual(typeof xhr2.responseText, 'string', 'Response text mocked');
clearGlobalProps('hit');
done();
};

xhr1.send();
// use timeout to avoid hit collisions
setTimeout(() => xhr2.send(), 10);
});
} else {
test('unsupported', (assert) => {
assert.ok(true, 'Browser does not support it');
Expand Down

0 comments on commit 7de58b6

Please sign in to comment.