How to debug XHR occurring an Angular 10 application - xmlhttprequest

By default Angular 10 uses an XHR backend, but there is a JSONP backend available.
I am receiving some odd results from the HttpClient, which differ from the data received when using other tools to interact with the backend API.
I want to be able to see what is happening in the raw XMLHttpRequest traffic. I suspect a HttpInterceptor is doing something funky, and when I add my own HttpInterceptor the payload has already been altered from what I expect to see. Watching the XHR will "prove" this theory.

The approach I have taken to adding some logging is to copy the XHR backend from https://github.com/angular/angular/blob/master/packages/common/http/src/xhr.ts into a subclass. Small changes needed to be made because not everything is exported from '#angular/common/http'.
(Note the following is MIT licensed
import { HttpXhrBackend, HttpHeaders, HttpRequest, HttpResponse, HttpBackend, HttpEvent } from '#angular/common/http';
import { Injectable } from '#angular/core';
import { Observable, Observer } from 'rxjs';
import {HttpDownloadProgressEvent, HttpErrorResponse, HttpEventType, HttpHeaderResponse, HttpUploadProgressEvent} from '#angular/common/http';
const XSSI_PREFIX = /^\)\]\}',?\n/;
/**
* Determine an appropriate URL for the response, by checking either
* XMLHttpRequest.responseURL or the X-Request-URL header.
*/
function getResponseUrl(xhr: any): string|null {
if ('responseURL' in xhr && xhr.responseURL) {
return xhr.responseURL;
}
if (/^X-Request-URL:/m.test(xhr.getAllResponseHeaders())) {
return xhr.getResponseHeader('X-Request-URL');
}
return null;
}
#Injectable()
export class CustomHttpBackend extends HttpXhrBackend {
handle(req: HttpRequest<any>): Observable<HttpEvent<any>> {
console.log(req);
// Quick check to give a better error message when a user attempts to use
// HttpClient.jsonp() without installing the HttpClientJsonpModule
if (req.method === 'JSONP') {
throw new Error(
`Attempted to construct Jsonp request without HttpClientJsonpModule installed.`);
}
// Everything happens on Observable subscription.
return new Observable((observer: Observer<HttpEvent<any>>) => {
// Start by setting up the XHR object with request method, URL, and withCredentials flag.
const xhr = new XMLHttpRequest();
xhr.open(req.method, req.urlWithParams);
if (!!req.withCredentials) {
xhr.withCredentials = true;
}
// Add all the requested headers.
Array.from(req.headers.keys())
.forEach(key => xhr.setRequestHeader(key, req.headers.get(key)!));
// Add an Accept header if one isn't present already.
if (!req.headers.has('Accept')) {
xhr.setRequestHeader('Accept', 'application/json, text/plain, */*');
}
// Auto-detect the Content-Type header if one isn't present already.
if (!req.headers.has('Content-Type')) {
const detectedType = req.detectContentTypeHeader();
// Sometimes Content-Type detection fails.
if (detectedType !== null) {
xhr.setRequestHeader('Content-Type', detectedType);
}
}
// Set the responseType if one was requested.
if (req.responseType) {
const responseType = req.responseType.toLowerCase();
// JSON responses need to be processed as text. This is because if the server
// returns an XSSI-prefixed JSON response, the browser will fail to parse it,
// xhr.response will be null, and xhr.responseText cannot be accessed to
// retrieve the prefixed JSON data in order to strip the prefix. Thus, all JSON
// is parsed by first requesting text and then applying JSON.parse.
xhr.responseType = ((responseType !== 'json') ? responseType : 'text') as any;
}
// Serialize the request body if one is present. If not, this will be set to null.
const reqBody = req.serializeBody();
// If progress events are enabled, response headers will be delivered
// in two events - the HttpHeaderResponse event and the full HttpResponse
// event. However, since response headers don't change in between these
// two events, it doesn't make sense to parse them twice. So headerResponse
// caches the data extracted from the response whenever it's first parsed,
// to ensure parsing isn't duplicated.
let headerResponse: HttpHeaderResponse|null = null;
// partialFromXhr extracts the HttpHeaderResponse from the current XMLHttpRequest
// state, and memoizes it into headerResponse.
const partialFromXhr = (): HttpHeaderResponse => {
if (headerResponse !== null) {
return headerResponse;
}
// Read status and normalize an IE9 bug (https://bugs.jquery.com/ticket/1450).
const status: number = xhr.status === 1223 ? 204 : xhr.status;
const statusText = xhr.statusText || 'OK';
// Parse headers from XMLHttpRequest - this step is lazy.
const headers = new HttpHeaders(xhr.getAllResponseHeaders());
// Read the response URL from the XMLHttpResponse instance and fall back on the
// request URL.
const url = getResponseUrl(xhr) || req.url;
// Construct the HttpHeaderResponse and memoize it.
headerResponse = new HttpHeaderResponse({headers, status, statusText, url});
return headerResponse;
};
// Next, a few closures are defined for the various events which XMLHttpRequest can
// emit. This allows them to be unregistered as event listeners later.
// First up is the load event, which represents a response being fully available.
const onLoad = () => {
// Read response state from the memoized partial data.
let {headers, status, statusText, url} = partialFromXhr();
// The body will be read out if present.
let body: any|null = null;
if (status !== 204) {
// Use XMLHttpRequest.response if set, responseText otherwise.
body = (typeof xhr.response === 'undefined') ? xhr.responseText : xhr.response;
}
console.log(body);
// Normalize another potential bug (this one comes from CORS).
if (status === 0) {
status = !!body ? 200 : 0;
}
// ok determines whether the response will be transmitted on the event or
// error channel. Unsuccessful status codes (not 2xx) will always be errors,
// but a successful status code can still result in an error if the user
// asked for JSON data and the body cannot be parsed as such.
let ok = status >= 200 && status < 300;
// Check whether the body needs to be parsed as JSON (in many cases the browser
// will have done that already).
if (req.responseType === 'json' && typeof body === 'string') {
// Save the original body, before attempting XSSI prefix stripping.
const originalBody = body;
body = body.replace(XSSI_PREFIX, '');
try {
// Attempt the parse. If it fails, a parse error should be delivered to the user.
body = body !== '' ? JSON.parse(body) : null;
} catch (error) {
// Since the JSON.parse failed, it's reasonable to assume this might not have been a
// JSON response. Restore the original body (including any XSSI prefix) to deliver
// a better error response.
body = originalBody;
// If this was an error request to begin with, leave it as a string, it probably
// just isn't JSON. Otherwise, deliver the parsing error to the user.
if (ok) {
// Even though the response status was 2xx, this is still an error.
ok = false;
// The parse error contains the text of the body that failed to parse.
body = {error, text: body};// as HttpJsonParseError
}
}
}
if (ok) {
// A successful response is delivered on the event stream.
observer.next(new HttpResponse({
body,
headers,
status,
statusText,
url: url || undefined,
}));
// The full body has been received and delivered, no further events
// are possible. This request is complete.
observer.complete();
} else {
// An unsuccessful request is delivered on the error channel.
observer.error(new HttpErrorResponse({
// The error in this case is the response body (error from the server).
error: body,
headers,
status,
statusText,
url: url || undefined,
}));
}
};
// The onError callback is called when something goes wrong at the network level.
// Connection timeout, DNS error, offline, etc. These are actual errors, and are
// transmitted on the error channel.
const onError = (error: ProgressEvent) => {
const {url} = partialFromXhr();
const res = new HttpErrorResponse({
error,
status: xhr.status || 0,
statusText: xhr.statusText || 'Unknown Error',
url: url || undefined,
});
observer.error(res);
};
// The sentHeaders flag tracks whether the HttpResponseHeaders event
// has been sent on the stream. This is necessary to track if progress
// is enabled since the event will be sent on only the first download
// progress event.
let sentHeaders = false;
// The download progress event handler, which is only registered if
// progress events are enabled.
const onDownProgress = (event: ProgressEvent) => {
// Send the HttpResponseHeaders event if it hasn't been sent already.
if (!sentHeaders) {
observer.next(partialFromXhr());
sentHeaders = true;
}
// Start building the download progress event to deliver on the response
// event stream.
let progressEvent: HttpDownloadProgressEvent = {
type: HttpEventType.DownloadProgress,
loaded: event.loaded,
};
// Set the total number of bytes in the event if it's available.
if (event.lengthComputable) {
progressEvent.total = event.total;
}
// If the request was for text content and a partial response is
// available on XMLHttpRequest, include it in the progress event
// to allow for streaming reads.
if (req.responseType === 'text' && !!xhr.responseText) {
progressEvent.partialText = xhr.responseText;
}
// Finally, fire the event.
observer.next(progressEvent);
};
// The upload progress event handler, which is only registered if
// progress events are enabled.
const onUpProgress = (event: ProgressEvent) => {
// Upload progress events are simpler. Begin building the progress
// event.
let progress: HttpUploadProgressEvent = {
type: HttpEventType.UploadProgress,
loaded: event.loaded,
};
// If the total number of bytes being uploaded is available, include
// it.
if (event.lengthComputable) {
progress.total = event.total;
}
// Send the event.
observer.next(progress);
};
// By default, register for load and error events.
xhr.addEventListener('load', onLoad);
xhr.addEventListener('error', onError);
xhr.addEventListener('timeout', onError);
xhr.addEventListener('abort', onError);
// Progress events are only enabled if requested.
if (req.reportProgress) {
// Download progress is always enabled if requested.
xhr.addEventListener('progress', onDownProgress);
// Upload progress depends on whether there is a body to upload.
if (reqBody !== null && xhr.upload) {
xhr.upload.addEventListener('progress', onUpProgress);
}
}
// Fire the request, and notify the event stream that it was fired.
xhr.send(reqBody!);
observer.next({type: HttpEventType.Sent});
// This is the return from the Observable function, which is the
// request cancellation handler.
return () => {
// On a cancellation, remove all registered event listeners.
xhr.removeEventListener('error', onError);
xhr.removeEventListener('abort', onError);
xhr.removeEventListener('load', onLoad);
xhr.removeEventListener('timeout', onError);
if (req.reportProgress) {
xhr.removeEventListener('progress', onDownProgress);
if (reqBody !== null && xhr.upload) {
xhr.upload.removeEventListener('progress', onUpProgress);
}
}
// Finally, abort the in-flight request.
if (xhr.readyState !== xhr.DONE) {
xhr.abort();
}
};
});
}
}
Then in app.module.ts, add the custom provider.
import { HttpBackend } from '#angular/common/http';
import { CustomHttpBackend } from './path/to/custom-http-backend';
providers: [
...,
{
provide: HttpBackend,
useClass: CustomHttpBackend,
},
],

Related

Axios cancelToken cancel and delete while sending multi requests strange behavior

I'm using axios cancelToken in order to cancel requests made by the user in order to not overload the api. But for some reason I'm struggling to understand why it works only for odd number of requests and if the object of the remains with a reference. In the following code I'm experiencing glitches, it misses most of the requests.
some of the functions are mutations some actions
async add({ dispatch, commit, state }, { item }) {
addLoader({id:item.id});
let { data } = await this.$axios.get("/add", {
params: item,
cancelToken: state.loaders[item.id].token,
});
//commit adding the item
},
// every token is being created here
function createToken(id = null) {
let cancelToken = null;
cancelToken = this.$axios.CancelToken.source();
return cancelToken;
}
addLoader(state, { id }) {
if (state.loaders[id]) {
state.loaders[id].cancel();
}
state.loaders = {
...state.loaders,
[id]: createToken.call(this),
};
},
removeLoader(state, { id }) {
let loadersObj;
let loader;
if (state.loaders[id]) {
loadersObj = { ...state.loaders };
loaders[id].cancel();
delete loaders[id];
// cannot delete before keeping a reference for some reason
// loader = loadersObj[id];
// loader.cancel();
// delete loadersObj[id];
////////////////////////
state.loaders = loadersObj;
} else {
console.log("didnt find any loader", id);
}
},
So basically if I'm using the code that i've marked it works perfectly for odd number of requets:
request1 - canceled
request2 - canceled
request3 - passed
when in even number of requests:
request1 - canceled
request2 - canceled
Appreciate it if you can guide me through to understand exactly what am I missing.
Thanks.

How to get all event logs of the contract in tron network using tronweb in node js without any limit?

How to get all events logs of the contract in tron network using tronweb in node js without any limit? or is there need of any middle ware storage like redis, etc?
Need to get all data at once before loading dapp home page. The dApp is made in react js. And Trongrid api have this limit of 200 records in single request.
You can use fingerprint (it works like continue token)
async getContractTransferEventsByUser(eventName, userId) {
let result = [];
let tronGrid = new TronGrid(this.tronWeb);
try {
let continueToken = '';
while (true) {
let res = await tronGrid.contract.getEvents(YOUR_CONTRACT_ADDRESS, {
only_confirmed: true,
event_name: eventName,
limit: 200,
fingerprint: continueToken,
order_by: "timestamp,asc",
min_timestamp: minTime, //remove if you don't need it
filters: { id: userId.toString() } //if you need to filter events by one or more values, for example, by user id (if this information is presented in event log), remove if you don't need it.
});
if (!res.success) {
console.warn("Can't get events for the contract");
break;
}
result = result.concat(res.data);
if (typeof res.meta.fingerprint !== 'undefined') {
continueToken = res.meta.fingerprint;
} else {
break;
}
}
} catch (error) {
console.error(error);
} finally {
return result;
}
},

Can't get click_action to work on FCM notifications with web app / PWA

I'm trying to get my "click_action" to take users to specific URLs on notifications that I'm sending to clients, but whatever I do it either does nothing (desktop) or just opens the PWA (android). The messages are coming through fine (checked in Chrome console) but clicking just doesn't seem to work.
I have the following in my service worker, cribbed from various places including other answers provided on this site:
importScripts('https://www.gstatic.com/firebasejs/7.14.3/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/7.14.3/firebase-messaging.js');
// importScripts('/__/firebase/init.js');
/* An empty service worker! */
self.addEventListener('fetch', function(event) {
/* An empty fetch handler! */
});
var firebaseConfig = {
//REDACTED
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
const messaging = firebase.messaging();
messaging.setBackgroundMessageHandler(function(payload) {
console.log('[firebase-messaging-sw.js] Received background message ', payload);
// Customize notification here
notificationTitle = payload.notification.title;
notificationOptions = {
body: payload.notification.body,
icon: payload.notification.icon,
click_action: payload.notification.click_action
};
return self.registration.showNotification(notificationTitle,
notificationOptions);
});
self.addEventListener('notificationclick', function(event) {
let url = event.notification.click_action;
// I've also added a data.click_action field in my JSON notification, and have tried using that
// instead, but that didn't work either
console.log('On notification click: ', event.notification.tag);
event.notification.close(); // Android needs explicit close.
event.waitUntil(
clients.matchAll({ includeUncontrolled: true, type: 'window' }).then( windowClients => {
// Check if there is already a window/tab open with the target URL
for (var i = 0; i < windowClients.length; i++) {
var client = windowClients[i];
// If so, just focus it.
if (client.url === url && 'focus' in client) {
return client.focus();
}
}
// If not, then open the target URL in a new window/tab.
if (clients.openWindow) {
return clients.openWindow(url);
}
})
);
});
self.onnotificationclick = function(event) {
let url = event.notification.click_action;
console.log('On notification click: ', event.notification.tag);
event.notification.close();
// This looks to see if the current is already open and
// focuses if it is
event.waitUntil(clients.matchAll({ includeUncontrolled: true, type: 'window' }).then(function(clientList) {
for (var i = 0; i < clientList.length; i++) {
var client = clientList[i];
if (client.url == url && 'focus' in client)
return client.focus();
}
if (clients.openWindow)
return clients.openWindow(url);
}));
};
The notifications come through fine on both android (installed PWA) and chrome, and the message payload in the developer console is well formatted and received fine. In the message I'm sending from the server I have a URL with a custom parameter on the end (e.g. https://[domain]/list.php?userid=123) but, as above, clicking on the notification doesn't do anything on windows/chrome, and on the android it opens the PWA successfully but then doesn't go to the URL in the payload, it just goes to wherever the PWA was when last open. The "userid" changes depending on the message trigger.
Sample JSON of message payload:
{data: {…}, from: "xxx", priority: "high", notification: {…}, collapse_key: "do_not_collapse"}
collapse_key: "do_not_collapse"
data: {gcm.notification.badge: "[logo URL]", click_action: "https://[URL]/list.php?userid=33"}
from: "xxx"
notification:
body: "'5' has just been added"
click_action: "https://[URL]/list.php?userid=33"
icon: "https://[logo URL]"
title: "alert "
I also saw something about "webpush": { "fcm_options": { "link": "https://dummypage.com"}} on https://firebase.google.com/docs/cloud-messaging/js/receive but couldn't figure out if that was relevant or needed also.
Am very surprised just providing a URL in the click_action doesn't seem to just do that action when you click the notificaiton! Is anything needed in the service worker at all?!?!
Could one of the problems be that the PWA doesn't update the SW regularly, and so if my code above should work (a big if!) then i just need to wait for the SW to update on the installed android app? If so, is there a way to speed up its updating?!?
Thanks so much in advance for any assistance. Am tying myself in knots here!
I spent a lot of time looking for a solution for the same problem. Maybe this can help :
if you send notification with firebase messaging, you can use webpush field. firebase messaging client library execute self.registration.showNotification() ... No more need messaging.onBackgroundMessage in your service worker.
// firebabse-coud-function.js
app.messaging().send({
webpush: {
notification: {
title: notification?.title || "Default title",
icon: notification?.icon || "/icon.png",
badge: notification?.icon || "/icon.png",
},
fcmOptions: {
link: `${BASE_URL || ""}${notification?.clickAction || "/"}`,
}
},
data: {
userID: notification.userID,
link: notification?.clickAction || "/",
},
topic
});
Most importantly, in your service worker add a 'notificationclick' event listener before calling firebase.messaging()
so my service worker looks like:
// firebase-messaging-sw.js
// ...
self.addEventListener('notificationclick', function (event) {
console.debug('SW notification click event', event)
const url = event.notification?.data?.FCM_MSG?.data?.link;
// ...
})
const messaging = firebase.messaging();
messaging.onBackgroundMessage(function (payload) {
// received others messages
})
For me, clicking on the event does not go to the correct url. So i add this:
// background client - service worker
const channel = new BroadcastChannel('sw-messages');
self.addEventListener('notificationclick', function (event) {
console.debug('SW notification click event', event)
const url = event.notification?.data?.FCM_MSG?.data?.link;
channel.postMessage({
type: 'notification_clicked',
data: {
title: event.notification.title,
clickAction: url
}
});
})
// foreground client
const channel = new BroadcastChannel('sw-messages');
channel.addEventListener("message", function (event) {
// go the page
})
I hope this helps someone.
This question and other answers seems to be related to the legacy FCM API, not the v1.
In those case, I needed the SW to open any url sent by FCM, which is by default not possible because host differs (see here).
Also, the notification object as changed, and the url for the webpush config is there now: event.notification.data.FCM_MSG.notification.click_action
So adapting others answers to get the correct field and open the url by only editing the firebase-messaging-sw.js:
importScripts('https://www.gstatic.com/firebasejs/8.2.10/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/8.2.10/firebase-messaging.js');
// Initialize the Firebase app in the service worker by passing in
// your app's Firebase config object.
// https://firebase.google.com/docs/web/setup#config-object
firebase.initializeApp({
...
})
self.addEventListener('notificationclick', function(event) {
event.notification.close();
// fcp_options.link field from the FCM backend service goes there, but as the host differ, it not handled by Firebase JS Client sdk, so custom handling
if (event.notification && event.notification.data && event.notification.data.FCM_MSG && event.notification.data.FCM_MSG.notification) {
const url = event.notification.data.FCM_MSG.notification.click_action;
event.waitUntil(
self.clients.matchAll({type: 'window'}).then( windowClients => {
// Check if there is already a window/tab open with the target URL
for (var i = 0; i < windowClients.length; i++) {
var client = windowClients[i];
// If so, just focus it.
if (client.url === url && 'focus' in client) {
return client.focus();
}
}
// If not, then open the target URL in a new window/tab.
if (self.clients.openWindow) {
console.log("open window")
return self.clients.openWindow(url);
}
})
)
}
}, false);
const messaging = firebase.messaging();
(register the addEventListener before initializing messaging)
Just add addeventlistner notification click event before calling firebase.messaging()
Everything will work fine.
importScripts('https://www.gstatic.com/firebasejs/8.4.1/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/8.4.1/firebase-messaging.js');
self.onnotificationclick = function(event) {
console.log('On notification click: ', event.notification.tag);
event.notification.close();
// This looks to see if the current is already open and
// focuses if it is
event.waitUntil(clients.matchAll({
type: "window"
}).then(function(clientList) {
for (var i = 0; i < clientList.length; i++) {
var client = clientList[i];
if (client.url == '/index' && 'focus' in client)
return client.focus();
}
if (clients.openWindow)
return clients.openWindow('/index');
}));
};
var firebaseConfig = {
apiKey: "xcxcxcxcxcxc",
authDomain: "xcxcxc.firebaseapp.com",
projectId: "fdsfdsdfdf",
storageBucket: "dfsdfs",
messagingSenderId: "sdfsdfsdf",
appId: "sdfsdfsdfsdfsdfsdf"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
const messaging = firebase.messaging();

Angular GlobalErrorHandler and HttpErrorResponse - Resolver throwing badly formatted HttpErrorResponse

I've created global error handler in my Angular 6 application:
main error handler method:
handleError(error: Error | HttpErrorResponse) {
const router = this.injector.get(Router);
const notificationService = this.injector.get(NotificationsService);
this._logger(error);
if (!navigator.onLine) {
notificationService.displayNotification('error', 'timespan', {heading: 'Internet connection lost!', body: ''});
} else if (error instanceof HttpErrorResponse) {
notificationService.displayNotification('error', 'click', this._httpErrorMessage(error));
} else {
// CLIENT error
router.navigate(['/error-page']);
}
}
Problem:
Many of HTTP service calls are being performed in resolvers:
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<ClientDetailsModel> {
if (route.params.cif) {
const reqBody = new GetClientDetailsRequestModel({cif: route.params.cif, idWewPrac: this.userContext.getUserSKP()});
return this.clientsService.getClientDetails(reqBody)
.pipe(
map((clientDetails: { customerDetails: ClientDetailsModel }) => {
if (clientDetails.customerDetails) {
return clientDetails.customerDetails;
}
return null;
})
);
}
If Http error occurs in such a call, error received by my global error handler is formed as HttpErrorResponse wrapped inside Error (message of Error is HttpErrorResponse):
Uncaught (in promise): HttpErrorResponse: {"headers":{"normalizedNames":{},"lazyUpdate":null},"status":400,"statusText":"OK","url":"https://...
If Http errors occurs outside of resolvers global error handler works perfectly fine.
To reach my goal (throwing HttpErrorResponse from resolver) I need to specify the way to handle error in error callback inside subscription, but I cannot do it because resolver is the one who manages subscription.
Is there a way to specify how resolver should handle errors?
I would like to avoid manual parsing of these wrapped errors.
I was searching for a solution, but could only create a work-a-round.
This will check for the HttpErrorResponse text and tries to parse the JSON which results into the real error object.
Not great at all, but better then nothing.
handleError(error: any): void {
console.error('Errorhandler catched error: ' + error.message, error);
// We need to have this little hack in oder to access the real error object
// The Angular resolver / promise wraps the error into the message, serialized as json.
// So we extract this error again.
// But first lets check if we actually dealing with an HttpErrorResponse ...
if (error.message.search('HttpErrorResponse: ')) {
// The error includes an HTTPErrorResponse, so we try to parse it's values ...
const regex = new RegExp('^.*HttpErrorResponse:\\s(\\{.*\\})$');
const matches = regex.exec(error.message);
if (matches !== null) {
// matches the regex, convert...
const httpErrorResponse = JSON.parse(matches[1]); // This is now the real error object with all the fields
this.handleHttpErrorResponse(httpErrorResponse);
} else {
// It contains HttpErrorResponse, but no JSON part...
this.toastr.error('There was an unknown communication error',
'Communication error',
{timeOut: 10000});
}
} else {
this.toastr.error('Unknown error occured',
'Well that should not happen. Check the log for more information...',
{timeOut: 10000});
}
}

Override/Intercept XMLHttpRequest response in all browsers

What do I want to achieve ?
I want to intercept the XMLHttpRequest and modify the response for some particular requests. (For ex. decrypt content and assign it to back response)
What I have done so far ?
Below code intercepts the request and modifies the response. It works in all browsers (Chrome, Firefox, Opera, Edge) except IE 11.
const dummySend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function () {
const _onreadystatechange = this.onreadystatechange;
this.onreadystatechange = function () {
if (this.readyState === 4) {
if (this.status === 200 || this.status === 1223) {
// as response is read-only and configurable, make it writable
Object.defineProperty(this, 'response', {writable: true});
this.response = modifyResponse(this.response);
}
}
if (_onreadystatechange) {
_onreadystatechange.apply(this, arguments);
}
}
dummySend.apply(__self, arguments);
}
What is the Issue ?
All of that doesn't work only in IE 11, The Error thrown is 'TypeError: Assignment to read-only property is not allowed in strict mode'.
Can someone please help me with this ?
I could do it the other way, which is to have a dummy XMLHttpRequest object exposed to the original requester and then handle the actual XMLHttpRequest yourself. Please read code for more clarity.
let oldXMLHttpRequest = window.XMLHttpRequest;
// define constructor for XMLHttpRequest proxy object
window.XMLHttpRequest = function() {
let _originalXhr = new oldXMLHttpRequest();
let _dummyXhr = this;
function decryptResponse(actualResponse) {
return base64Decrypted = decrypt(response, secret);
}
_dummyXhr.response = null;
// expose dummy open
_dummyXhr.open = function () {
const _arguments = [].slice.call(arguments);
// do any url modifications here before request open
_dummyXhr._url = _arguments[1];
return _originalXhr.open.apply(_originalXhr, _arguments);
};
// expose dummy send
_dummyXhr.send = function () {
let _onreadystatechange = _dummyXhr.onreadystatechange;
_originalXhr.onreadystatechange = function() {
if (this.readyState === 4 && (this.status === 200 || this.status === 1223)) {
_dummyXhr.response = decryptResponse(this.response);
}
// call callback that was assigned on our object
if (_onreadystatechange) {
_onreadystatechange.apply(_dummyXhr, arguments);
}
}
_originalXhr.send.apply(_originalXhr, arguments);
};
// iterate all properties in _originalXhr to proxy them according to their type
// For functions, we call _originalXhr and return the result
// For non-functions, we make getters/setters
// If the property already exists on _dummyXhr, then don't proxy it
for (let prop in _originalXhr) {
// skip properties we already have - this will skip both the above defined properties
// that we don't want to proxy and skip properties on the prototype belonging to Object
if (!(prop in _dummyXhr)) {
// create closure to capture value of prop
(function(prop) {
if (typeof _originalXhr[prop] === "function") {
// define our own property that calls the same method on the _originalXhr
Object.defineProperty(_dummyXhr, prop, {
value: function() {return _originalXhr[prop].apply(_originalXhr, arguments);}
});
} else {
// define our own property that just gets or sets the same prop on the _originalXhr
Object.defineProperty(_dummyXhr, prop, {
get: function() {return _originalXhr[prop];},
set: function(val) {_originalXhr[prop] = val;}
});
}
})(prop);
}
}