In a previous post about XMLHttpRequest we've describe how to capture requests so that we can examine the URLs, and possibly add our own headers. In this post, we will display a more intrusive method, where we create a proxy for the XMLHttpRequest, so that we can replace the request, and the response, as well as sending the data to a different URL.
The proxy structure is as follows:
const originalXhrClass = XMLHttpRequest
XMLHttpRequest = function () {
// the proxy code goes here
}
We replace the XMLHttpRequest with our own code, and keep an internal reference to the original XMLHttpRequest. Let's examine the proxy code. Let declare some fields that we will later use.
const originalXhrObject = new originalXhrClass()
const self = this
self.onreadystatechange = null
const doneEventHandlers = []
const requestHeaders = []
let lastDoneEvent
let changeResponseDone = false
let response
In the open method call we can replace the target URL:
Object.defineProperty(self, 'open', {
value: function () {
const url = arguments[1]
arguments[1] = `https://my.example.com/my/modified/url?url=${url}`
return originalXhrObject['open'].apply(originalXhrObject, arguments)
},
})
The addRequestHeader calls are kept for later use.
Object.defineProperty(self, 'setRequestHeader', {
value: function () {
const [name, value] = arguments
requestHeaders.push([name, value])
},
})
The methods/getters/setters that we do not want to change are passed through to the original object.
const getters = ['status', 'statusText', 'readyState', 'responseXML', 'upload']
getters.forEach(function (property) {
Object.defineProperty(self, property, {
get: function () {
return originalXhrObject[property]
},
})
})
const getterAndSetters = ['ontimeout, timeout', 'responseType', 'withCredentials', 'onload', 'onerror', 'onprogress']
getterAndSetters.forEach(function (property) {
Object.defineProperty(self, property, {
get: function () {
return originalXhrObject[property]
},
set: function (val) {
originalXhrObject[property] = val
},
})
})
const standardMethods = ['removeEventListener', 'abort', 'getAllResponseHeaders', 'getResponseHeader', 'overrideMimeType']
standardMethods.forEach(function (method) {
Object.defineProperty(self, method, {
value: function () {
return originalXhrObject[method].apply(originalXhrObject, arguments)
},
})
})
Upon send of the data, we can update the request body.
Object.defineProperty(self, 'send', {
value: async function () {
originalXhrObject.setRequestHeader('Content-Type', 'application/json')
const body = arguments[0]
const newBody = {
body,
requestHeaders,
}
arguments[0] = JSON.stringify(newBody)
return originalXhrObject['send'].apply(originalXhrObject, arguments)
},
})
The response is modified once it arrives.
async function modifyResponse() {
if (changeResponseDone) {
return
}
response = `my modified response${originalXhrObject.responseText}`
changeResponseDone = true
}
originalXhrObject.onreadystatechange = async function () {
if (originalXhrObject.readyState === 4) {
await modifyResponse()
}
if (self.onreadystatechange) {
return self.onreadystatechange()
}
if (lastDoneEvent) {
doneEventHandlers.forEach((handler) => {
handler(lastDoneEvent)
})
}
}
we supply response getters.
const responseGetters = ['response', 'responseText']
responseGetters.forEach(function (property) {
Object.defineProperty(self, property, {
get: function () {
return response
},
})
})
and we handle the notification per the addEventListener, and for the onload event.
Object.defineProperty(self, 'addEventListener', {
value: function () {
const eventType = arguments[0]
if (eventType === 'load') {
const handler = arguments[1]
doneEventHandlers.push(handler)
return
}
return originalXhrObject['addEventListener'].apply(originalXhrObject, arguments)
},
})
originalXhrObject.addEventListener('load', (event) => {
if (changeResponseDone) {
doneEventHandlers.forEach((handler) => {
setTimeout(() => {
handler(event)
}, 100)
})
} else {
lastDoneEvent = event
}
})
Final Note
We have shown how to proxy the XMLHttpRequest. A different simpler approach could be by using a service worker, where proxy will not be required, and hence there is no need to handle the various event listeners, and getters/setters.