MiddleMan - insert yourself between a page and its network traffic
This places a layer of middleware between a page's codebase and the fetch() and XMLHttpRequest APIs they pretty much all rely on.
Using it is simple. You get one object, middleMan
, and you can .addHook()
and .removeHook()
on it.
middleMan.addHook("https://example.com/your/route/*", {
requestHandler(request) {
console.log("snooped on request:", request);
}
});
Your request handler can return nothing to keep the current Request unchanged.
It can return a different Request object to change what will happen over the network.
It can also return a Response object, in which case the network request will be skipped altogether and the Response will be used instead.
Finally, it can return a Promise to either a Request or a Response.
Hooks are called in the order they were registered. Each hook is given the Request object obtained from the previous hook.
When a request hook returns a Response, no further request hooks are called.
middleMan.addHook(/https:\/\/example\.org\/foo/bar\/*.json/, {
async responseHandler(response) {
console.log("snooped on response:", response);
const data = await response.json();
data.topLevel = "IMPORTANT";
return Response.json(data);
}
});
In the example above, we used a regular expression for the route rather than a string with wildcards. Use whatever makes sense.
Response handlers work similarly to request handlers. They can return nothing to keep the current response, or they can return a new response.
All matching response hooks are called in order.
That's the gist of it.
Here's a little bit of middleware that logs all graphql requests and responses, and replaces "dog" with "cat" on Twitter:
middleMan.addHook("https://twitter.com/i/api/graphql/*", {
requestHandler(request) {
console.log("REQUEST HANDLER saw", request);
},
async responseHandler(response) {
console.log("RESPONSE HANDLER saw", response);
const data = await response.json();
console.log("data=", data);
function traverse(obj, check, mutate) {
if (!obj || typeof obj != 'object') return;
Object.keys(obj).forEach(k => {
if (check(obj, k)) obj[k]=mutate(obj[k]);
else traverse(obj[k], check, mutate);
});
}
traverse(data,
(obj,key) => ['full_text','text','description'].includes(key) && typeof obj[key] == 'string',
s=> s.replace(/\bdog\b/ig, 'cat'));
return Response.json(data);
}
});
It produces results like this:

That's it. Have fun!
(Disclaimer: I just put this together. It's probably buggy. Very buggy. Proceed with caution.)