📜 ⬆️ ⬇️

Substitution of XMLHttpRequest or not touching the tons of ready-made js-code to change the behavior of all ajax requests

Hello, in this small note I will tell you a little about OOP in JS, the XMLHttpRequest object, the proxy pattern, and the javascript friendliness in this regard.

Today I had such a task - there is a project that uses ajax-requests quite actively, but here’s the problem - the backend is so organized that it will not authenticate the user if he is not active for, say, half an hour. As a result, it happened that the user, trying to perform some kind of action that the Ajax uses, could not perform it (sorry for the tautology), it was necessary to solve this problem.


TK, which I set for myself

If an AJAX request is made and the answer is * something that says that the user has completed the session *, you need to display the login form to the user (with the usual overlay, without any i-frames), and allow him to authenticate through it (again by Ajaxu, because you can not lose the state of the page). Moreover, if it passes authentication, Ajax requests, which did not pass then, need to be sent again. But the first problem is that you need to do this so that the js code that relies on this query does not feel anything, that is, all the callbacks need to work as they need and when they need to (they should not work when it turns out that you need authentication, but must when it is passed). And the second problem comes from asynchronous requests - there can be a lot of them, it may happen that several requests will immediately encounter this problem, you need to monitor everyone and, if necessary, restart them after authentication. And, yes, * something that says that the user has completed the session * - in our case, this is the response code "403" and the response body "401" (401, because it’s close in spirit, but not because of the need in WWW-Authenticate, but simply 403 is impossible because in general it’s in no way connected with authentication, but at least it’s close).
')
How I love Javascript

Without hesitation, I came to a decision - using the “proxy” pattern, create the proxy object itself and replace XMLHttpRequest (XHR) with it, and in this proxy itself, communicate directly with XHR. And yes, imagine, in Javascript it is possible to replace the class with another class, in fact, here the class is the same object (or prototype, in terminology I am not strong :().
So for starters, how do you create a class in general that can be instantiated using new ClassName () and how do you add methods and properties there? There are several ways to see everything you can google , I probably used the simplest, this is what the definition of a class looks like with us:
(function () { "use strict"; window.SomeClass = function () { var randNumber = Math.random(); this.someMethod = function () { console.log(randNumber); }; this.randomized = randNumber; }; })(); 


If you noticed, I immediately packed all the code into a function that I immediately called, this is a common practice in JS, it is used firstly for cleanliness of the code (we don’t litter the global scop), and secondly because of performance (this follows from First, the fact is that when you create a lot of variables in one scope (in this case, global), then when you access them, the interpreter will search for them longer, because it will have to go through more options, because the search variable starts with the closest scoop and goes up to global). I also use use strict, you can read about it here , it will help you avoid unforeseen situations, especially if you use IDE (and especially if you use JsLint / JsHint). And about the code - as you can see we created the "class" SomeClass in the global scope, in fact, the constructor of this class is all the code inside the function. As a result, we have the randNumber variable, which is visible only from the inside of the class (more precisely, its instance), the method someMethod (), which always sends the same number to the console for the same instance of the class, and the randomized property, which is the same number .
Making a substitution:
 (function () { "use strict"; //   , ..       var XHR = window.XMLHttpRequest; window.XMLHttpRequest = function () { //    var o = new XHR(), t = this, reassignAllProperties = function reassign() { t.readyState = o.readyState; t.responseText = o.responseText; t.responseXML = o.responseXML; t.status = o.status; t.statusText = o.statusText; }; t.readyState = 0; t.responseText = ""; t.responseXML = null; t.status = null; t.statusText = ""; //    ,     //    reassignAllProperties() ..  //        - - t.open = function open() { o.open.apply(o, arguments); reassignAllProperties(); }; t.send = function send() { o.send.apply(o, arguments); reassignAllProperties(); }; t.abort = function abort() { o.abort(); reassignAllProperties(); }; t.setRequestHeader = o.setRequestHeader; t.overrideMimeType = o.overrideMimeType; t.getResponseHeader = o.getResponseHeader; t.getAllResponseHeaders = o.getAllResponseHeaders; t.onreadystatechange = function () {}; o.onreadystatechange = function onReady() { reassignAllProperties(); t.onreadystatechange(); }; }; })(); 


As can be seen from the code, it completely repeats the original XMLHttpRequest (a summary of the methods / St.-you can look at the Russian Wikipedia page ). We also need a lot more - we need to monitor the server’s response, and if we notice the 403rd response and 401 in the body, then we urgently open the login form. But the custom callback should not be called. And moreover, after the “abortion” there should be an opportunity to restart the request and get an answer. Therefore, we must store in the proxy object all data that was passed to any setter methods (including open and send) and when the query is restarted, we need to re-call all these methods. But the problem remains - imagine a situation when a request was made that failed due to razauturification, then, only after the user logged in, we have to restart the request and start the onreadystatechange event, but this event should not be triggered before the request will be restarted. The solution is simple - the fact is that the onreadystatechange event is triggered at least four times, while the readyState property is incremented (all its values ​​are here ), so we need to call the user callback only when we are sure that the answer is legitimate. But, if states other than “complete” are used somewhere, you need to take this into account, the easiest way is to run the event three times with the last three readyState (from 2 to 4), just in a loop. You also need to store all requests that failed, which will need to be restarted after that.

Finishing touches

 (function () { "use strict"; //   , ..       var XHR = window.XMLHttpRequest, //       ,    failedRequestsPool = [], authenticationWindow = function () { $("#auth-overlay").show(); }; $("#auth-overlay form").submit(function () { $.ajax({ type: "post", url: "/login", data: { login: $("#auth-login").val(), password: $("#auth-password").val() }, dataType: "json", success: function (data) { if (data.state === "OK") { $("#auth-overlay").hide(); //   ,      for (var i in failedRequestsPool) { if (failedRequestsPool.hasOwnProperty(i)) { failedRequestsPool[i].retry(); } } failedRequestsPool = []; } } }); return false; }); window.XMLHttpRequest = function () { //    var o = new XHR(), t = this, //   ,       callback- aborted = false, reassignAllProperties = function reassign() { t.readyState = o.readyState; t.responseText = o.responseText; t.responseXML = o.responseXML; t.status = o.status; t.statusText = o.statusText; }, //      ,       data = { open: null, send: null, setRequestHeader: [], overrideMimeType: null }; t.readyState = 0; t.responseText = ""; t.responseXML = null; t.status = null; t.statusText = ""; t.retry = function retry() { aborted = false; //     o.open.apply(o, data.open); reassignAllProperties(); for (var i in data.setRequestHeader) { if (data.setRequestHeader.hasOwnProperty(i)) { o.setRequestHeader.apply(o, data.setRequestHeader[i]); } } if ("overrideMimeType" in o && data.overrideMimeType !== null) { o.overrideMimeType(data.overrideMimeType); } o.send(data.send); reassignAllProperties(); }; //    ,     //    reassignAllProperties() ..  //        - - t.open = function open() { data.open = arguments; // ,    o.open.apply(o, arguments); reassignAllProperties(); }; t.send = function send(body) { data.send = body; o.send(body); reassignAllProperties(); }; t.abort = function abort() { o.abort(); reassignAllProperties(); }; t.setRequestHeader = function setRequestHeader() { data.setRequestHeader.push(arguments); o.setRequestHeader.apply(o, arguments); }; //    IE     ,   if ("overrideMimeType" in o) { t.overrideMimeType = function (mime) { data.overrideMimeType = mime; o.overrideMimeType(mime); }; } t.getResponseHeader = o.getResponseHeader; t.getAllResponseHeaders = o.getAllResponseHeaders; t.onreadystatechange = function () {}; o.onreadystatechange = function onReady() { reassignAllProperties(); //          ,  ,   if (!aborted && o.state === 403 && o.responseText.indexOf("401") !== -1) { aborted = true; o.abort(); failedRequestsPool.push(t); authenticationWindow(); } //        ,    if (!aborted && o.readyState === 4) { for (var i = 1; i < 5; ++i) { t.readyState = i; t.onreadystatechange(); } } }; }; })(); 


So, quite simply, we replaced XHR with a proxy, which will not allow to miss the requests sent after the user’s “unauthentication”.

PS I noticed one error, the getAllResponseHeaders () method in the opera throws out WRONG_THIS_ERR, it’s just completely unclear where it came from.
Faculty staff let's not discuss how it was better to solve the original problem. The fact is that this solution can be useful not only in the case of ratifying

UPD If you really need to make such a substitution (and I advise you to think before this, because, as many have noticed in the comments, replacing the root libraries is not very good), then use this solution github.com/ilinsky/xmlhttprequest

Source: https://habr.com/ru/post/148140/


All Articles