Would it be possible to determine when a particular XMLHttpRequest has completed using pure JavaScript?. Say I know the URL of a resource that will be requested with an XMLHttpRequest, for example: http://www.mywebsite.com/test.json, could I track the completion of this request using JavaScript?
This is the code I have so far:
var oldXHR = window.XMLHttpRequest;
function newXHR() {
var realXHR = new oldXHR();
realXHR.addEventListener("readystatechange", function() {
if(realXHR.readyState==4 && realXHR.status==200){
console.log('request was made');
}
}, false);
return realXHR;
}
window.XMLHttpRequest = newXHR;
however, this tracks every single HTTP request made on a website and isn't very useful. I want to pinpoint the exact request and determine when it has completed. How could this be done?
You need to "hijack" (override, practically) the open() method instead of the constructor.
XMLHttpRequest.prototype.open = (function(realOpen) {
return function(method, url) {
if (url === 'http://www.mywebsite.com/test.json') {
this.addEventListener('readystatechange', function() {
// ...do what you have to do
});
}
realOpen.apply(this, arguments);
};
})(XMLHttpRequest.prototype.open);
Related
I want to intercept all XHR requests being sent, and change their URL and headers before the request gets sent.
Found this similar question but there are no answers there.
I tried hooking XMLHttpRequest.prototype.open, But it only gives me access to the response:
(function () {
var origOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function () {
console.log(arguments); // prints method ("GET"), URL
console.log(this); // prints response, responseText, responseURL, status, statusText, and onXXX handlers
origOpen.apply(this, arguments);
};
})();
Also tried hooking XMLHttpRequest.prototype.setRequestHeader, but it only gives me access to each header value being set, one by one, and I can't associate it to the URL of the request:
(function () {
var origSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
XMLHttpRequest.prototype.setRequestHeader = function (header, value) {
console.log("header", header);
console.log("value", value);
origSetRequestHeader.apply(this, arguments);
};
})();
I managed to hook XMLHttpRequest.prototype.send to set a custom header, but since I want to change an existing header key, it appends my new value instead of replacing the existing one. Other people encountered the same problem: 1, 2:
(function () {
var origSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function () {
arguments[1] = myNewUrl; // arguments[1] holds the URL
this.setRequestHeader('existingHeaderKey', newHeaderValue)
origSend.apply(this, arguments);
};
})();
How can I accomplish this?
The XMLHttpRequest(xhr) interface exposes a very few things. So there is limitation on what you can intercept.
However, we can wrap the xhr objects in Proxy and collect data until send is called. And before sending the request we modify the data at one spot.
const OriginalXHR = XMLHttpRequest;
// wrap the XMLHttpRequest
XMLHttpRequest = function() {
return new Proxy(new OriginalXHR(), {
open(method, url, async, username = null, password = null) {
lg('open');
// collect URL and HTTP method
this.modMethod = method;
this.modUrl = url;
this.open(...arguments);
},
setRequestHeader(name, value) {
lg('set header');
if (!this.modReqHeaders) {
this.modReqHeaders = {};
}
// collect headers
this.modReqHeaders[name] = value;
// do NOT set headers here. Hold back!
// this.setRequestHeader(name, value);
},
send(body = null) {
lg('processing request...');
// do the final processing
// ...
// don't forget to set headers
for (const [name, value] of Object.entries(this.modReqHeaders)) {
this.setRequestHeader(name, value);
}
lg('sending request =>' +
'\n\t\tmethod: \t' + this.modMethod +
'\n\t\turl:\t\t' + this.modUrl +
'\n\t\theaders:\t' + JSON.stringify(this.modReqHeaders));
this.send(body);
},
get(xhr, key) {
if (!key in xhr) return undefined;
let value = xhr[key];
if (typeof value === "function") {
// if wrapped, use the function in proxy
value = this[key] || value;
return (...args) => value.apply(xhr, args);
} else {
//return properties
return value;
}
},
set(xhr, key, value) {
if (key in xhr) {
xhr[key] = value;
}
return value;
}
});
}
console.warn('XMLHttpRequest has been patched!\n XMLHttpRequest: ', XMLHttpRequest);
let url = 'https://baconipsum.com/api/?type=all-meat&sentences=1&start-with-lorem=1';
function getData() {
console.log('fetching lorem ipsum');
let xhr = new XMLHttpRequest();
xhr.responseType = 'json';
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
document.getElementById("demo").innerText = this.response[0];
}
};
xhr.open("GET", url, true);
xhr.setRequestHeader('Referer', 'www.google.com');
xhr.setRequestHeader('Accept-Encoding', 'x-compress; x-zip')
xhr.setRequestHeader('Accept-Language', 'de-US,en;q=0.5');
xhr.send();
}
//fancy logging, looks good in dark mode
function lg(msg) {
console.log('%c\t Proxy: ' + msg, 'background: #222; color: #bada55');
}
#demo {
min-height: 100px;
background-color: wheat;
}
<button onclick="getData()">Get data</button>
<div id="demo"></div>
<p>Note: look in the Developer Console for debug logs</p>
You can wrap remaining xhr methods or attributes in the proxy handler as per your requirement.
This may not be as good as service workers. But service workers have following drawbacks:
A service worker is run in a worker context: it therefore has no DOM access, and runs on a different thread to the main JavaScript that powers your app, so it is non-blocking. It is designed to be fully async; as a consequence, APIs such as synchronous XHR and Web Storage can't be used inside a service worker.
Service workers only run over HTTPS, for security reasons. Having modified network requests, wide open to man in the middle attacks would be really bad. In Firefox, Service Worker APIs are also hidden and cannot be used when the user is in private browsing mode.ref
I have been trying to override the methods in order to intercept the xhr requests but seems like everything I do it only prinds '1' for this.readyState.
Does anyone have any idea why?
addInterceptorsToXHRRequests() {
const originalOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function() {
const originalStateChangeHandler = this.onreadystatechange;
this.onreadystatechange = function() {
console.log(' ---> ',this.readyState);
if (originalStateChangeHandler instanceof Function) {
originalStateChangeHandler.apply(this, arguments);
console.log('Ready state: ' + this.readyState);
}
};
return originalOpen.apply(this, arguments);
};
}
I am calling this function from index.js, at the end of componentDidMount life cycle method. (pretty big project)
This happens because you bind a function on XMLHttpRequest.prototype.open .
open returns only state 1.
So, you need to attach the onreadystatechange function to the full xhr.
Example
// you probably want the original XMLHttpRequest here...
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
console.log(xhr.readyState);
};
Say I'm interested in checking the params that were sent with the XMLHttpRequest.
For instance, if I sent a POST petition with param 'option=1' can I retrieve that from the response?
I checked for the methods and properties but haven't seen a way to get it.
Fire a XMLHTTPRequest and examine the response object in your browser's JS console (F12 for Chrome/Firefox).
I believe the data is not there, at least I once changed the XMLHttpRequest open() method for a project (of course, I might have been just too stupid to find it). That way, my default error handler knows the original URL when printing errors to the user/sending errors to the error reporting backend.
Rough code snippet, pulled from a projects init-code:
/**
* Check XMLHttpRequest availability
*/
var ajax = null;
var proto = null;
if (window.XMLHttpRequest) {
ajax = new XMLHttpRequest ();
proto = XMLHttpRequest.prototype;
} else if (window.ActiveXObject) {
try {
ajax = new ActiveXObject("Msxml2.XMLHTTP.6.0");
proto = ActiveXObject("Msxml2.XMLHTTP.6.0").prototype;
} catch (e) { }
}
if (ajax == null) {
alert ("Can not create AJAX object. You need a more recent browser!");
return;
}
/**
* Update ajax prototype to store the URL (for better error handling)
*/
try {
var origOpen = proto.open;
proto.open = function (method, url) {
this._url = url;
return origOpen.apply (this, arguments);
}
} catch (e) {
console.log ("Can not patch XMLHttpRequest to store URL. Console output will omit them...");
}
You would need to adapt this for POST data passed to the send() function instead. Be aware, that the method is probably bad style, and my JS style might be even worse!
Better: But you could always pass the POST data directly to the callback function, without storing it in the XMLHTTPRequest object:
var postData = "SomeStuff-Foobar123";
var ajax = new XMLHttpRequest (); //add magic for other browsers here
ajax.open ("POST", "ajax.php", true);
ajax.onreadystatechange = function () {
if (this.readyState != 4 || this.status != 200) {
console.log ("Not ready, yet...");
return 0;
}
//response is in this.responseText
//but you can still access the parent objects!
console.log ("Done with Code 200. POSTed data: " + postData);
}
ajax.send (postData);
As Bergi said it's not possible to retrieve the parameters that were sent with the request on the response. So I'm closing the question.
Thanks to everyone who helped!
Just to point out, I know how to do this with jQuery and AngularJS. The project I am currently working on requires me to use plain JavaScript.
I'm trying to get AJAX to work with just plain JavaScript. I am using Java/Spring for backend programming. Here is my JavaScript code:
/** AJAX Function */
ajaxFunction = function(url) {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (xhttp.status == 200) {
var JSONResponse = JSON.parse(xhttp.responseText);
return JSONResponse;
}
}
xhttp.open('GET', url, true);
xhttp.send();
}
/** Call Function */
searchResults = function() {
var test = ajaxFunction('http://123.456.78.90:8080/my/working/url');
console.log(test);
}
/** When the page loads. */
window.onload = function() {
searchResults();
}
It's worth noting that when I go directly to the URL in my browser's address bar (example, if I go directly to the link http://123.456.78.90:8080/my/working/url), I get a JSON response in the browser.
When I hover over xhttp.status, the status is saying 0, not 200, even though I know that the link I am calling works. Is this something that you have to set in Spring's controllers? I didn't think that was the case because when I inspect this JS URL call in the Network tab, it states that the status is 200.
All in all, this response is coming back as undefined. I can't figure out why. What am I doing wrong?
An XMLHttpRequest is made asynchronously meaning that the request is fired off and the rest of the code continues to run. A callback is provided and when the asynchronous operation completes the callback function is called. The onreadystatechange function is called upon completion of an AJAX request. In your example the ajaxFunction will return immediately after the xhttp.send() line executes, so your var test won't have the JSON in it as I assume you expect.
In order to do something when an AJAX request completes you need to use a callback function. If you wanted to log the result to the console as above you could try something like the following:
var xhttp;
var handler = function() {
if(xhttp.readyState === XMLHttpRequest.DONE) {
if (xhttp.status == 200) {
var JSONResponse = JSON.parse(xhttp.responseText);
console.log(JSONResponse);
}
}
};
/** AJAX Function */
var ajaxFunction = function(url) {
xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = handler;
xhttp.open('GET', url, true);
xhttp.send();
};
/** Call Function */
var searchResults = function() {
ajaxFunction('http://123.456.78.90:8080/my/working/url');
};
/** When the page loads. */
window.onload = function() {
searchResults();
};
If you want to learn more about how XMLHttpRequest works then MDN is a much better teacher than I am :)
I'm trying something very simple for my first Firefox Add-On, the important part is:
Step 1) Call an external API to retrieve some data.
Step 2) Call that API again with the data retrieved the first time to get some more.
Now, I first implemented it using XMLHttpRequest in synchronous mode, since I thought the need to wait for Step 2 forced me to do it that way. Two calls to the function that dealt with the API call, used XMLHttpRequest and parsed the response. Fine.
Then I came accross various docs in the Mozilla Development Network which encourage you to use XMLHttpRequest in asynchronous mode and so I tried.
Basing my implementation on multiple XMLHttpRequests and others I came up with the code below.
My question is: Is this the proper way to do it? Should I go back to using synchronous mode? It works like this, but it just doesn't strike me as the correct AJAX pattern you would use...
// first call
var username = foo;
var password = bar;
var startOffset = 0; // initial value
var url = encodeURIComponent('https://theapiurl.com/query=' + startOffset);
doRequest();
function doRequest() {
makeRequest(url, username, password);
}
function makeRequest(url, username, password) {
var http_request = new XMLHttpRequest();
if (http_request.overrideMimeType) {
http_request.overrideMimeType('text/xml');
}
if (!http_request) {
alert('Cannot create XMLHTTP instance');
return false;
}
http_request.onreadystatechange = function() {
alertContents(http_request);
};
http_request.open('GET', url, true, username, password);
http_request.send(null);
}
function alertContents(http_request) {
if (http_request.readyState == 4) {
if (http_request.status == 200) {
if (startOffset == 0) {
startOffset = 45; // this value would be extracted from 'http_request'
url = encodeURIComponent('https://theapiurl.com/query=' + startOffset);
// second call, parameter startOffset has changed
doRequest();
} else {
}
} else {
alert('There was a problem with the request.');
}
http_request.onreadystatechange = function fnNull(){};
}
}
You should always avoid doing synchronous network requests as it will block the GUI from functioning until you get a response. Just because the network may be fast for you, you should not assume it will be fast for all of your users.