How can I use Promises in Aurelia? - javascript

I have 2 files
1. api_service.ts
import { HttpClient } from 'aurelia-http-client';
import { autoinject } from "aurelia-framework";
import { ObserverLocator } from "aurelia-binding";
#autoinject()
export class ApiServices {
private app_url = 'http://example.com/';
private host = 'http://api.example.com:8080';
constructor(private store: Store, private client: HttpClient, private observerLocator: ObserverLocator) { }
GetInstallationId() {
let url = this.host + "/1/api/iid";
let iid;
this.client.get(url)
.catch((err) => { })
.then(res => {
let response = JSON.parse(res.response);
iid = response.iid;
});
return iid;
}
}
here I am trying to get some value from the server and in the HTTP response, I can see that I am getting it.
board_services.ts
import { Store } from './localstorage_service';
import { ApiServices } from './api_service';
import { autoinject } from "aurelia-framework";
import { ObserverLocator } from "aurelia-binding";
#autoinject()
export class AppServices {
private installation_ID: string;
constructor(private store: Store, private apiServices: ApiServices, private observerLocator: ObserverLocator) { }
getInstallationID() {
const iid = this.store.getValue('installation_ID');
console.log("iid = " + iid);
if (iid && iid != undefined) {
console.log('inside if iid');
// this.installation_ID = this.apiServices.GetInstallationId();
this.installation_ID = iid;
} else {
console.log('inside else iid');
this.installation_ID = this.apiServices.GetInstallationId();
}
this.store.setValue('installation_ID', this.installation_ID);
console.log("after if condition iid = " + iid);
return this.installation_ID;
}
}
here in this code I'm calling the function GetInstallationId() from api_services.ts but it is not waiting for the http request to complete. Is there a way to make the statement in the next statement execute after the GetInstallationId() is executed?

The short answer: you need to support asynchronous behavior.
You may do this via (a) Promises and/or (b) async/await. The former is supported in all browsers except IE 11 and there are polyfills like this one if you need to support IE 11. The latter is currently not supported in IE at all and as it requires new JS keywords/syntax must be used with a transpiler like Babel.
This answer will use Promises as they have fewer dependencies than async/await. The snippets below have been updated to use Promises -- look for the comments like:
// REPLACED THIS LINE ... // WITH THIS LINE
and
// ADDED THIS LINE
which identify the updates.
GetInstallationId is an asynchronous function and must return a promise.
class ApiServices {
...
GetInstallationId() {
let url = this.host + "/1/api/iid";
return new Promise(function(accept, reject) {
let iid;
this.client.get(url)
.catch((err) => { })
.then(res => {
let response = JSON.parse(res.response);
// REPLACED THIS LINE
// iid = response.iid;
// WITH THIS LINE which accepts the promise
accept(response.iid);
})
// ADDED THIS LINE to catch any errors
// e.g. when invalid response received
.catch(reject);
});
}
}
Because getInstallationID calls GetInstallationId, it too must return a promise. This example defines a separate function, handler, which is then bound via .bind(this) to ensure that whenever the function is executed, this will always refer back to the correct ApiService object.
class AppServices {
...
getInstallationID() {
function handler(accept, reject) {
const iid = this.store.getValue('installation_ID');
console.log("iid = " + iid);
if (iid && iid != undefined) {
console.log('inside if iid');
// this.installation_ID = this.apiServices.GetInstallationId();
this.installation_ID = iid;
} else {
console.log('inside else iid');
// REPLACED THIS LINE
// this.installation_ID = this.apiServices.GetInstallationId();
// WITH THIS LINE
this.apiServices.GetInstallationId().then(accept,reject);
}
this.store.setValue('installation_ID', this.installation_ID);
console.log("after if condition iid = " + iid);
// REPLACED THIS LINE
// return this.installation_ID;
// WITH THIS LINE
accept(this.installation_ID);
};
return new Promise(handler.bind(this));
}
...
}
The above function is just one example which will work cross browser. Another solution which requires ES6 support and/or transpilation is to use arrow functions:
//
// Arrow function version
//
class AppServices {
...
getInstallationID() {
// NOTE the => which is shorthand for `(function (accept,reject) {...}).bind(this)`
return new Promise((accept, reject) => {
const iid = this.store.getValue('installation_ID');
console.log("iid = " + iid);
if (iid && iid != undefined) {
console.log('inside if iid');
// this.installation_ID = this.apiServices.GetInstallationId();
this.installation_ID = iid;
} else {
console.log('inside else iid');
// REPLACED THIS LINE
// this.installation_ID = this.apiServices.GetInstallationId();
// WITH THIS LINE
this.apiServices.GetInstallationId().then(accept,reject);
}
this.store.setValue('installation_ID', this.installation_ID);
console.log("after if condition iid = " + iid);
// REPLACED THIS LINE
// return this.installation_ID;
// WITH THIS LINE
accept(this.installation_ID);
});
}
...
}
With the above refactoring, you can now call getInstallationID() from e.g. another view model like this:
import {AppServices} from '...';
import {autoinject} from 'aurelia-framework';
#autoinject()
export class SomeViewModel {
constructor(appServices) {
this.appServices = appServices;
}
callGetInstallationId() {
this.appServices.getInstallationID().then(function(id) {
// the ID that is passed here is the one that is `accept`ed.
// ie. if AppServices.getInstallationID calls accept('123'), then
// the '123' will be passed to this function as the first argument
}, function(e) {
// this function will be called if AppServices.getInstallationID
// calls reject. e will be the Error object, if any, that is passed
// to the reject method.
});
}
}

Related

Angular 12 with ERROR TypeError: this.handleError is not a function

I am developing a web APP using Angular 12. I used a global error handler to process all my http errors. In my book portal component (in book module), When I called ReaderService
function (getFavorList) from another reader module, got an error: TypeError: this.handleError is not a function. If I call this function in same module is fine, and if I changed catchError code in getFavorList from
catchError(this.handleError('getFavorBookList'))
to
catchError((err)=>console.log(err))
This error will also disappeared, looks like "this" has problem, but I don't know how to fix without changing the error handling function. I also tried to bind(this) to this.handleError but not fixing the issue.
Following is code of book portal component:
ngOnInit(): void {
const bookID = this.route.snapshot.paramMap.get('id');
const readerName = this.tokenService.getUsername();
this.commonService.setSubject(readerName);
const readerID = this.readerAuthService.getReaderID();
//Load book info from database
this.bookService.getBook(bookID).subscribe((eBook: Book) => {
if (eBook && eBook.bookTitle) {
this.book = eBook;
this.logger.info(`Success load profile of ${bookID}`)
} else {
this.logger.warn(`Failed to load ${bookID} profile from server`);
}
//Load the existing comments and display in page
console.log('The comment length is ' + eBook.comments.length);
const existComments = document.querySelector('div.existing-comments');
if (eBook.comments.length > 0) {
this.bookService.getBookComments(bookID).subscribe((comments: BookComment[]) => {
if (comments && comments.length > 0) {
this.logger.info(`Success load comments of book ${bookID}`)
for (const item of comments) {
let p1 = document.createElement('p');
p1.className = 'comment-item';
p1.innerHTML = item.comment;
p1.style.fontSize = 'large';
p1.style.fontFamily = 'Times New Roman';
let p2 = document.createElement('p');
p2.className = 'comment-item';
p2.innerHTML = `---by ${item.readerName}`;
p2.style.fontSize = 'large';
p2.style.fontFamily = 'Times New Roman';
existComments.appendChild(p1);
existComments.appendChild(p2);
}
}
});
} else {
let p1 = document.createElement('p');
p1.className = 'comment-item';
p1.innerHTML = 'Be the first person to write comments!';
p1.style.fontSize = 'large';
p1.style.fontFamily = 'Times New Roman';
existComments.appendChild(p1);
}
this.commentForm.setValue({
bookID: bookID,
readerName: readerName,
title: '',
comment: '',
});
});
//If book is in favor book list, disable add fovoriteBook button
let favorInd = false;
this.readerService.getFavorList(readerID).subscribe((data) => {
console.log(data);
if (data && data.length > 0) {
for (const item of data) {
if (item.bookID === bookID) {
favorInd = true;
break;
}
}
if (favorInd) {
const addFavorButton = document.querySelector('button.add-favorites') as HTMLButtonElement;
addFavorButton.disabled = true;
}
}
});
}
Following is code of getFavorList:
private handleError: HandleError;
getFavorList(readerID): Observable<any> {
return this.http.get(`/api/reader/${readerID}/getfavourlist`).pipe(
catchError(this.handleError('getFavorBookList')), shareReplay()
)
}
Following is code of hanleError part:
export type HandleError =
<T> (operation?: string, result?: T) => (error: HttpErrorResponse) => Observable<T>;
/** Handles HttpClient errors */
#Injectable()
export class HttpErrorHandler {
constructor(
private router: Router,
private logger: NGXLogger,
) { }
createHandleError = (serviceName = '') => {
return <T>(operation = 'operation', result = {} as T) => this.handleError(serviceName, operation, result)
}
/**
* Returns a function that handles Http operation failures.
* This error handler lets the app continue to run as if no error occurred.
* #param serviceName = name of the data service that attempted the operation
* #param operation - name of the operation that failed
* #param result - optional value to return as the observable result
*/
handleError<T>(serviceName = '', operation = 'operation', result = {} as T) {
return (error: HttpErrorResponse): Observable<T> => {
//Generate error message
let errorMessage = '';
if (error.error instanceof ErrorEvent) {
errorMessage = `Client side error: ${error.error.message}`;
} else if (error.status === 401) {
errorMessage = `Server return ${error.status} with body "${error.error}"`;
if (error.error.message.includes('Incorrect username or password')) {
window.alert('Incorrect username or password, please check');
} else {
window.alert('Need login to access the contents.');
if (serviceName.toLowerCase().indexOf('reader') != -1) {
this.router.navigateByUrl('/reader/login');
} else {
this.router.navigateByUrl('/librarian/login');
}
}
} else {
errorMessage = `Server return ${error.status} with body "${error.error}"`;
}
//Generate user friendly error log
const errorLog = `HTTP Error in ${serviceName}: ${operation} failed: ${errorMessage}`;
// TODO: send the error to remote logging infrastructure
this.logger.error(errorLog);
return of(result);
};
}
}
Thanks!

Asynchronous function inside asynchronous function

I have this block of code in a function:
this.apiService.fetchCategories(!this.cacheData).subscribe(
response => {
if(this._jsnValService.valCategories(response)) {
this.customerMap.categories = this.formatCategories(response["categories"]);
} else {
alert("Categories failed the schema validation. Please contact support if this happens again.");
}
},
error => {
this.notification.title = "Oops, there's a problem.";
this.notification.content = "Seems there's an issue getting the provider categories.";
this.notification.show("provider_categories_api");
}
);
It fetches some data and then runs a validation on the data (if(this._jsnValService.valCategories(response)) {).
However, my validation of the data is actually async too, because it validates it against a json schema which is in a seperate json file so it has to read the file first.
I have used a promise to read the file contents and then do the validation:
#Injectable()
export class ValidateJSONSchemaService {
constructor(private http: Http) {}
public valCategories(json) {
this._getSchema("./jsonSchema.categories.json").then((schema) => {
this._valSchema(json, schema);
});
};
private _valSchema(json, schema): any {
var ajv = new Ajv();
var valid = ajv.validate(schema, json);
if (!valid) {
console.log(ajv.errors);
return false;
} else {
console.log(valid);
return true;
};
};
private _getSchema(fileName): any {
return new Promise((resolve, reject) => {
this.http.get(fileName)
.map(this._extractData)
.catch(this._handleError)
.subscribe(schema => resolve(schema));
});
};
private _extractData(res: Response) {
let body = res.json();
return body.data || {};
};
How can I edit the top code block in this question to account for the asynchronous function inside the if statement (if(this._jsnValService.valCategories(response)) {)?
if you are using ES6 you could use async/await like this:
async function _validateCategories() {
this.apiService.fetchCategories(!this.cacheData).subscribe(
response => {
const valid = await this._jsnValService.valCategories(response)
if(valid) {
this.customerMap.categories = this.formatCategories(response["categories"]);
} else {
alert("Categories failed the schema validation. Please contact support if this happens again.");
}
},
error => {
this.notification.title = "Oops, there's a problem.";
this.notification.content = "Seems there's an issue getting the provider categories.";
this.notification.show("provider_categories_api");
}
);
}
if not, your function fetchCategories should return a promise or allow you to pass a callback to do something like this:
async function _validateCategories() {
this.apiService.fetchCategories(!this.cacheData).subscribe(
response => {
this._jsnValService.valCategories(response).then((error, valid)=> {
if(error) {
alert("Categories failed the schema validation. Please contact support if this happens again.");
}
this.customerMap.categories = this.formatCategories(response["categories"]);
})
},
error => {
this.notification.title = "Oops, there's a problem.";
this.notification.content = "Seems there's an issue getting the provider categories.";
this.notification.show("provider_categories_api");
}
);
}

How to wait for a function to finish its execution in angular 2.?

Below is my code, I want login() and authenticated() functions to wait for getProfile() function to finish its execution. I tried several ways like promise etc. but I couldn't implement it. Please suggest me the solution.
import { Injectable } from '#angular/core';
import { tokenNotExpired } from 'angular2-jwt';
import { myConfig } from './auth.config';
// Avoid name not found warnings
declare var Auth0Lock: any;
#Injectable()
export class Auth {
// Configure Auth0
lock = new Auth0Lock(myConfig.clientID, myConfig.domain, {
additionalSignUpFields: [{
name: "address", // required
placeholder: "enter your address", // required
icon: "https://example.com/address_icon.png", // optional
validator: function(value) { // optional
// only accept addresses with more than 10 chars
return value.length > 10;
}
}]
});
//Store profile object in auth class
userProfile: any;
constructor() {
this.getProfile(); //I want here this function to finish its work
}
getProfile() {
// Set userProfile attribute if already saved profile
this.userProfile = JSON.parse(localStorage.getItem('profile'));
// Add callback for lock `authenticated` event
this.lock.on("authenticated", (authResult) => {
localStorage.setItem('id_token', authResult.idToken);
// Fetch profile information
this.lock.getProfile(authResult.idToken, (error, profile) => {
if (error) {
// Handle error
alert(error);
return;
}
profile.user_metadata = profile.user_metadata || {};
localStorage.setItem('profile', JSON.stringify(profile));
this.userProfile = profile;
});
});
};
public login() {
this.lock.show();
this.getProfile(); //I want here this function to finish its work
};
public authenticated() {
this.getProfile(); //I want here this function to finish its work
return tokenNotExpired();
};
public logout() {
// Remove token and profile from localStorage
localStorage.removeItem('id_token');
localStorage.removeItem('profile');
this.userProfile = undefined;
};
}
Like you saw in the comments, you have to use Promise or Observable to achieve this, since your behaviour is pretty simple, you should use Promise because Observable will have a lot of features you don't need in this case.
Here is the Promise version of your service:
import { Injectable } from '#angular/core';
import { tokenNotExpired } from 'angular2-jwt';
import { myConfig } from './auth.config';
// Avoid name not found warnings
declare var Auth0Lock: any;
#Injectable()
export class Auth {
// Configure Auth0
lock = new Auth0Lock(myConfig.clientID, myConfig.domain, {
additionalSignUpFields: [{
name: "address", // required
placeholder: "enter your address", // required
icon: "https://example.com/address_icon.png", // optional
validator: function(value) { // optional
// only accept addresses with more than 10 chars
return value.length > 10;
}
}]
});
//Store profile object in auth class
userProfile: any;
constructor() {
this.getProfile(); //I want here this function to finish its work
}
getProfile():Promise<void> {
return new Promise<void>(resolve => {
// Set userProfile attribute if already saved profile
this.userProfile = JSON.parse(localStorage.getItem('profile'));
// Add callback for lock `authenticated` event
this.lock.on("authenticated", (authResult) => {
localStorage.setItem('id_token', authResult.idToken);
// Fetch profile information
this.lock.getProfile(authResult.idToken, (error, profile) => {
if (error) {
// Handle error
alert(error);
return;
}
profile.user_metadata = profile.user_metadata || {};
localStorage.setItem('profile', JSON.stringify(profile));
this.userProfile = profile;
resolve()
});
});
})
};
public login(): Promise<void>{
this.lock.show();
return this.getProfile(); //I want here this function to finish its work
};
public authenticated():void{
this.getProfile().then( () => {
return tokenNotExpired();
});
};
public logout():void {
// Remove token and profile from localStorage
localStorage.removeItem('id_token');
localStorage.removeItem('profile');
this.userProfile = undefined;
};
}
More on Promise here
I would recommend that you set up getProfile to return an observable. Then your other functions can subscribe to that function and do their actions in the subscribe function. The Angular 2 HTTP tutorial gives an example of this

node.js redis and how to use promise when using a module

I have an Express route like this in an node server (file is required):
var redis = require('../modules/redis');
module.exports = function (app) {
var redisClient = redis.init();
app.post('/auth/ticket', cors(), function (req, res) {
var hashes = ['hash1','hash2', 'hash3'];
var candidates = []; // An array to collect valid hashes
var key;
// to check each hash against a RedisDB I use a For Loop
for (key in hashes) {
var hash = hashes[key];
console.log("Hash " + hash + " will be proofed now:");
//now I try to collect the valid hashes in the candidates array
if (redisClient.exists(hash) === 1) candidates.push(hash);
}
console.log(JSON.stringify(candidates));
});
};
Now here is the code of my module which shall manage all the redis requests:
exports.init = function () {
Redis = exports.Redis = function () {
var promiseFactory = require("q").Promise,
redis = require('promise-redis')(promiseFactory);
this.client = redis.createClient();
this.client.on('error', function (err) {
console.log('redis error – ' + client.host + ':' + client.port + ' – ' + err);
});
Redis.prototype.exists = function (key) {
this.client.exists(key, function (err, data) {
return data === 1 ? true : false;
});
};
return new Redis();
};
So what I experience is that the module is able to console.log the results properly. If a hash is valid, it returns true and otherwise false. This works as expected.
Problem is, that the for-loop continuous the execution without fetching getting the results. I think this is caused by race-conditions.
As you can see, I have started to workout something there with the use of Q and promise-redis in the top of my code:
var promiseFactory = require("q").Promise,
redis = require('promise-redis')(promiseFactory);
this.client = redis.createClient();
I like to know, how I make my for-loop (in the Express route) waiting for the results of redisClient.exists(hash) or in other words, to get all valid hashes into my candidates array.
Please help
like #brad said, you could use Q.all, it would take an array of promises as input and then return an array of results when all the promises are finished:
there is a mistake in your answer:
Redis.prototype.exists = function (key) {
return this.client.exists(key) // CHANGED, you still need to return a promise.
.then(function (reply) {
console.log("reply " + reply);
return (reply);
})
.catch(console.log);
};
If I understand correctly, what you want is something like
exports.init = function () {
Redis = exports.Redis = function () {
var Q = require("q"),
promiseFactory = Q.Promise,
redis = require('promise-redis')(promiseFactory);
this.client = redis.createClient();
this.client.on('error', function (err) {
console.log('redis error – ' + client.host + ':' + client.port + ' – ' + err);
});
Redis.prototype.exists = function (key) {
return this.client.exists(key).then(function (data) {
return data === 1 ? true : false;
});
};
Redis.prototype.getActive = function (arry) {
var self = this;
return Q.all(arry.map(self.exists.bind(self))
).then(function(res){
return arry.filter(function(val, idx){ return res[idx];});
});
};
return new Redis();
};
# mido22: But did you also recognize that I outsourced all the reds functions to the module file (1st Codeblock) which requires the promise-redid and builds a factory for Q. I changed the code inside the module file to:
Redis.prototype.exists = function (key) {
this.client.exists(key)
.then(function (reply) {
console.log("reply " + reply);
return (reply);
})
.catch(console.log);
};
and this results correctly like the console.log evidently shows.
Your codechange of the for-loop works very well but I think it don't fulfills my needs perfectly. If I could, I would like to have it completely outsourced in to the module file, so that I can use the prototyped method in similar cases from everywhere. Is that possible anyhow?
I see, that it would result in having two promise supported functionalities, if I would create an Instance of Redis Client with promise-redid and Q inside the auth/ticket/ router, too.
like this:
var Q = require('q'),
promiseFactory = Q.Promise,
redis = require("promise-redis")(promiseFactory),
client;
an then the express route (there are a lot of more routes each in a single file) like in your code.
Do you understand what I mean? Of course your solution will be fine for my needs at all, but a module resolving the job completely could have more elegance if possible so far.
Using with redis, bluebird and typescript:
import { RedisClient, createClient, ClientOpts } from "redis";
import { promisifyAll, PromisifyAllOptions } from "bluebird";
export module FMC_Redis {
export class Redis {
opt: ClientOpts;
private rc: RedisClient;
private rcPromise: any;
private static _instance: Redis = null;
public static current(_opt?: ClientOpts): Redis {
if (!Redis._instance) {
Redis._instance = new Redis(_opt);
Redis._instance.redisConnect();
}
return Redis._instance;
}
public get client(): RedisClient {
if (!this.rc.connected) throw new Error("There is no connection to Redis DB!");
return this.rc;
}
/******* BLUEBIRD ********/
public get clientAsync(): any {
// promisifyAll functions of redisClient
// creating new redis client object which contains xxxAsync(..) functions.
return this.rcPromise = promisifyAll(this.client);
}
private constructor(_opt?: ClientOpts) {
if (Redis._instance) return;
this.opt = _opt
? _opt
: {
host: "127.0.0.1",
port: 6379,
db: "0"
};
}
public redisConnect(): void {
this.rc = createClient(this.opt);
this.rc
.on("ready", this.onReady)
.on("end", this.onEnd)
.on("error", this.onError);
}
private onReady(): void { console.log("Redis connection was successfully established." + arguments); }
private onEnd(): void { console.warn("Redis connection was closed."); }
private onError(err: any): void { console.error("There is an error: " + err); }
/****** PROMISE *********/
// promise redis test
public getRegularPromise() {
let rc = this.client;
return new Promise(function (res, rej) {
console.warn("> getKeyPromise() ::");
rc.get("cem", function (err, val) {
console.log("DB Response OK.");
// if DB generated error:
if (err) rej(err);
// DB generated result:
else res(val);
});
});
}
/******* ASYNC - AWAIT *******/
// async - await test function
public delay(ms) {
return new Promise<string>((fnResolve, fnReject) => {
setTimeout(fnResolve("> delay(" + ms + ") > successfull result"), ms);
});
}
public async delayTest() {
console.log("\n****** delayTest ")
let a = this.delay(500).then(a => console.log("\t" + a));
let b = await this.delay(400);
console.log("\tb::: " + b);
}
// async - await function
public async getKey(key: string) {
let reply = await this.clientAsync.getAsync("cem");
return reply.toString();
}
}
}
let a = FMC_Redis.Redis.current();
// setTimeout(function () {
// console.warn(a.client.set("cem", "naber"));
// console.warn(a.client.get("cem"));
// console.warn(a.client.keys("cem"));
// }, 1000);
/***** async await test client *****/
a.delayTest();
/** Standart Redis Client test client */
setTimeout(function () {
a.client.get("cem", function (err, val) {
console.log("\n****** Standart Redis Client")
if (err) console.error("\tError: " + err);
else console.log("\tValue ::" + val);
});
}, 100)
/***** Using regular Promise with Redis Client > test client *****/
setTimeout(function () {
a.getRegularPromise().then(function (v) {
console.log("\n***** Regular Promise with Redis Client")
console.log("\t> Then ::" + v);
}).catch(function (e) {
console.error("\t> Catch ::" + e);
});
}, 100);
/***** Using bluebird promisify with Redis Client > test client *****/
setTimeout(function () {
var header = "\n***** bluebird promisify with Redis Client";
a.clientAsync.getAsync("cem").then(result => console.log(header + result)).catch(console.error);
}, 100);

How can I intercept XMLHttpRequests from a Greasemonkey script?

I would like to capture the contents of AJAX requests using Greasemonkey.
Does anybody know how to do this?
The accepted answer is almost correct, but it could use a slight improvement:
(function(open) {
XMLHttpRequest.prototype.open = function() {
this.addEventListener("readystatechange", function() {
console.log(this.readyState);
}, false);
open.apply(this, arguments);
};
})(XMLHttpRequest.prototype.open);
Prefer using apply + arguments over call because then you don't have to explicitly know all the arguments being given to open which could change!
How about modifying the XMLHttpRequest.prototype.open or send methods with replacements which set up their own callbacks and call the original methods? The callback can do its thing and then call the callback the original code specified.
In other words:
XMLHttpRequest.prototype.realOpen = XMLHttpRequest.prototype.open;
var myOpen = function(method, url, async, user, password) {
//do whatever mucking around you want here, e.g.
//changing the onload callback to your own version
//call original
this.realOpen (method, url, async, user, password);
}
//ensure all XMLHttpRequests use our custom open method
XMLHttpRequest.prototype.open = myOpen ;
Tested in Chrome 55 and Firefox 50.1.0
In my case I wanted to modify the responseText, which in Firefox was a read-only property, so I had to wrap the whole XMLHttpRequest object. I haven't implemented the whole API (particular the responseType), but it was good enough to use for all of the libraries I have.
Usage:
XHRProxy.addInterceptor(function(method, url, responseText, status) {
if (url.endsWith('.html') || url.endsWith('.htm')) {
return "<!-- HTML! -->" + responseText;
}
});
Code:
(function(window) {
var OriginalXHR = XMLHttpRequest;
var XHRProxy = function() {
this.xhr = new OriginalXHR();
function delegate(prop) {
Object.defineProperty(this, prop, {
get: function() {
return this.xhr[prop];
},
set: function(value) {
this.xhr.timeout = value;
}
});
}
delegate.call(this, 'timeout');
delegate.call(this, 'responseType');
delegate.call(this, 'withCredentials');
delegate.call(this, 'onerror');
delegate.call(this, 'onabort');
delegate.call(this, 'onloadstart');
delegate.call(this, 'onloadend');
delegate.call(this, 'onprogress');
};
XHRProxy.prototype.open = function(method, url, async, username, password) {
var ctx = this;
function applyInterceptors(src) {
ctx.responseText = ctx.xhr.responseText;
for (var i=0; i < XHRProxy.interceptors.length; i++) {
var applied = XHRProxy.interceptors[i](method, url, ctx.responseText, ctx.xhr.status);
if (applied !== undefined) {
ctx.responseText = applied;
}
}
}
function setProps() {
ctx.readyState = ctx.xhr.readyState;
ctx.responseText = ctx.xhr.responseText;
ctx.responseURL = ctx.xhr.responseURL;
ctx.responseXML = ctx.xhr.responseXML;
ctx.status = ctx.xhr.status;
ctx.statusText = ctx.xhr.statusText;
}
this.xhr.open(method, url, async, username, password);
this.xhr.onload = function(evt) {
if (ctx.onload) {
setProps();
if (ctx.xhr.readyState === 4) {
applyInterceptors();
}
return ctx.onload(evt);
}
};
this.xhr.onreadystatechange = function (evt) {
if (ctx.onreadystatechange) {
setProps();
if (ctx.xhr.readyState === 4) {
applyInterceptors();
}
return ctx.onreadystatechange(evt);
}
};
};
XHRProxy.prototype.addEventListener = function(event, fn) {
return this.xhr.addEventListener(event, fn);
};
XHRProxy.prototype.send = function(data) {
return this.xhr.send(data);
};
XHRProxy.prototype.abort = function() {
return this.xhr.abort();
};
XHRProxy.prototype.getAllResponseHeaders = function() {
return this.xhr.getAllResponseHeaders();
};
XHRProxy.prototype.getResponseHeader = function(header) {
return this.xhr.getResponseHeader(header);
};
XHRProxy.prototype.setRequestHeader = function(header, value) {
return this.xhr.setRequestHeader(header, value);
};
XHRProxy.prototype.overrideMimeType = function(mimetype) {
return this.xhr.overrideMimeType(mimetype);
};
XHRProxy.interceptors = [];
XHRProxy.addInterceptor = function(fn) {
this.interceptors.push(fn);
};
window.XMLHttpRequest = XHRProxy;
})(window);
You can replace the unsafeWindow.XMLHttpRequest object in the document with a wrapper. A little code (not tested):
var oldFunction = unsafeWindow.XMLHttpRequest;
unsafeWindow.XMLHttpRequest = function() {
alert("Hijacked! XHR was constructed.");
var xhr = oldFunction();
return {
open: function(method, url, async, user, password) {
alert("Hijacked! xhr.open().");
return xhr.open(method, url, async, user, password);
}
// TODO: include other xhr methods and properties
};
};
But this has one little problem: Greasemonkey scripts execute after a page loads, so the page can use or store the original XMLHttpRequest object during it's load sequence, so requests made before your script executes, or with the real XMLHttpRequest object wouldn't be tracked by your script. No way that I can see to work around this limitation.
I spent quite some time figuring out how to do this.
At first I was just overriding window.fetch but that stopped working for some reason - I believe it has to do with Tampermonkey trying to sandbox window (??) and I also tried unsafeWindow with the same results.
So. I started looking into overriding the requests at a lower level. The XMLHttpRequest (also that class name upper case lower case ew...)
Sean's answer was helpful to get started but didn't show how to override the responses after interception. The below does that:
let interceptors = [];
/*
* Add a interceptor.
*/
export const addInterceptor = (interceptor) => {
interceptors.push(interceptor);
};
/*
* Clear interceptors
*/
export const clearInterceptors = () => {
interceptors = [];
};
/*
* XML HTPP requests can be intercepted with interceptors.
* Takes a regex to match against requests made and a callback to process the response.
*/
const createXmlHttpOverride = (
open
) => {
return function (
method: string,
url,
async,
username,
password
) {
this.addEventListener(
"readystatechange",
function () {
if (this.readyState === 4) {
// Override `onreadystatechange` handler, there's no where else this can go.
// Basically replace the client's with our override for interception.
this.onreadystatechange = (function (
originalOnreadystatechange
) {
return function (ev) {
// Only intercept JSON requests.
const contentType = this.getResponseHeader("content-type");
if (!contentType || !contentType.includes("application/json")) {
return (
originalOnreadystatechange &&
originalOnreadystatechange.call(this, ev)
);
}
// Read data from response.
(async function () {
let success = false;
let data;
try {
data =
this.responseType === "blob"
? JSON.parse(await this.response.text())
: JSON.parse(this.responseText);
success = true;
} catch (e) {
console.error("Unable to parse response.");
}
if (!success) {
return (
originalOnreadystatechange &&
originalOnreadystatechange.call(this, ev)
);
}
for (const i in interceptors) {
const { regex, override, callback } = interceptors[i];
// Override.
const match = regex.exec(url);
if (match) {
if (override) {
try {
data = await callback(data);
} catch (e) {
logger.error(`Interceptor '${regex}' failed. ${e}`);
}
}
}
}
// Override the response text.
Object.defineProperty(this, "responseText", {
get() {
return JSON.stringify(data);
},
});
// Tell the client callback that we're done.
return (
originalOnreadystatechange &&
originalOnreadystatechange.call(this, ev)
);
}.call(this));
};
})(this.onreadystatechange);
}
},
false
);
open.call(this, method, url, async, username, password);
};
};
const main = () => {
const urlRegex = /providers/; // Match any url with "providers" in the url.
addInterceptor({
urlRegex,
callback: async (_data) => {
// Replace response data.
return JSON.parse({ hello: 'world' });
},
override: true
});
XMLHttpRequest.prototype.open = createXmlHttpOverride(
XMLHttpRequest.prototype.open
);
};
main();
Based on proposed solution I implemented 'xhr-extensions.ts' file which can be used in typescript solutions.
How to use:
Add file with code to your solution
Import like this
import { XhrSubscription, subscribToXhr } from "your-path/xhr-extensions";
Subscribe like this
const subscription = subscribeToXhr(xhr => {
if (xhr.status != 200) return;
... do something here.
});
Unsubscribe when you don't need subscription anymore
subscription.unsubscribe();
Content of 'xhr-extensions.ts' file
export class XhrSubscription {
constructor(
private callback: (xhr: XMLHttpRequest) => void
) { }
next(xhr: XMLHttpRequest): void {
return this.callback(xhr);
}
unsubscribe(): void {
subscriptions = subscriptions.filter(s => s != this);
}
}
let subscriptions: XhrSubscription[] = [];
export function subscribeToXhr(callback: (xhr: XMLHttpRequest) => void): XhrSubscription {
const subscription = new XhrSubscription(callback);
subscriptions.push(subscription);
return subscription;
}
(function (open) {
XMLHttpRequest.prototype.open = function () {
this.addEventListener("readystatechange", () => {
subscriptions.forEach(s => s.next(this));
}, false);
return open.apply(this, arguments);
};
})(XMLHttpRequest.prototype.open);
Not sure if you can do it with greasemonkey, but if you create an extension then you can use the observer service and the http-on-examine-response observer.

Categories

Resources