The problem#
I was writing a user script where I wanted to be able to
intercept the fetch()
calls the web page made so my script
could use the contents. I found a suggestion of
simply reassigning window.fetch
to my own function that internally
called the real window.fetch
while also doing whatever else I wanted.
While it worked fine under Tampermonkey on Chromium,
under Greasemonkey on Firefox, the script would just
silently fail with no indication of why the code wasn't running.
(The script I was writing was this one for fixing the formatting on the Folklife 2024 schedule to reformat the schedule to display as a grid. I plan to write a devlog post on it in the future, but just writing about the most pernicious issue in this post.)
The solution#
The problem was that Firefox's security model special-cases
function calls between web pages and extensions (and user scripts
running inside Greasemonkey count as part of an extension for this
purpose). And, furthermore, Promise
s can't pass the boundary, so you
need to carefully define functions such that the
implicit Promise
created by declaring the function async
lives on
the right side of the boundary.
The following code combines all of that together to intercept fetch()
on both Firefox and Chromium such that a userscript function intercept
is called with the text of the response to every fetch()
:
const intercept = responseText => {
// use responseText somehow ...
};
const w = window.wrappedJSObject;
if (w) {
exportFunction(intercept, window,
{ defineAs: "extIntercept" });
w.eval("window.origFetch = window.fetch");
w.eval(`window.fetch = ${async (...args) => {
let [resource, config] = args;
const response = await window.origFetch(resource,config);
window.extIntercept(await response.clone().text())
return response;
}}`);
} else {
const { fetch: origFetch } = window;
window.fetch = async (...args) => {
let [resource, config] = args;
const response = await origFetch(resource, config);
intercept(await response.clone().text());
return response;
};
}