Skip to content

Commit d8a0aeb

Browse files
committed
Make node module interception more reliable
This does two things: * Makes cached modules include modifications & mutations that we might've made, but injecting the modified module back into the module cache * Makes interception more resilient to multiple different requires. Previously if you required a module with the same name, we only fixed it once, even though it could be different instances (for different files), and a similar issue appeared with the above fix when requiring the same file repeatedly, if the recognized fixable name wasn't the first one used. We now do idempotent fixes instead, and fix everything every time.
1 parent 7687654 commit d8a0aeb

File tree

1 file changed

+34
-30
lines changed

1 file changed

+34
-30
lines changed

overrides/path/prepend.js

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,6 @@ const realLoad = mod._load;
4040
// it's deferred until the first require(), in case the user cares about
4141
// require order (e.g. if they're mocking out the 'http' module completely).
4242

43-
const alreadyIntercepted = [];
44-
4543
// Normally false, but can be set to a list of modules to defer interception so
4644
// we can use the required modules as part of the monkey patching process.
4745
// E.g. global-agent requires http & https, so we would otherwise get circular
@@ -51,34 +49,33 @@ let delayedInterception = false;
5149
// Given a just-loaded module's name, do whatever is required to set up interception
5250
// This must return the resulting module, but due to delayed interception, that isn't
5351
// used if it's hit in a circular require. Works for now, but needs careful thought.
54-
function fixModule(name, loadedModule) {
55-
if (name === 'http' || name === 'https') {
52+
// All fixes must be idempotent (must not cause problems if run repeatedly).
53+
function fixModule(requestedName, filename, loadedModule) {
54+
let fixedModule = loadedModule;
55+
56+
if (requestedName === 'http' || requestedName === 'https') {
5657
delayedInterception = [];
5758

5859
interceptAllHttp();
5960

6061
delayedInterception.forEach(function (modDetails) {
61-
fixModule(modDetails.name, modDetails.loadedModule);
62+
fixModule(modDetails.requestedName, modDetails.filename, modDetails.loadedModule);
6263
});
6364
delayedInterception = false;
64-
65-
return loadedModule;
66-
} else if (name === 'axios') {
65+
} else if (requestedName === 'axios') {
6766
// Disable built-in proxy support, to let global-agent/tunnel take precedence
6867
// Supported back to the very first release of Axios
69-
loadedModule.defaults.proxy = false;
70-
return loadedModule;
71-
} else if (name === 'request') {
68+
fixedModule.defaults.proxy = false;
69+
} else if (
70+
requestedName === 'request' &&
71+
loadedModule.defaults && // Request >= 2.17 (before that, proxy support isn't a problem anyway)
72+
!loadedModule.INTERCEPTED_BY_HTTPTOOLKIT // Make this idempotent
73+
) {
7274
// Disable built-in proxy support, to let global-agent/tunnel take precedence
73-
if (loadedModule.defaults) {
74-
return loadedModule.defaults({ proxy: false });
75-
} else {
76-
// Request < 2.17 (_very_ old). Predates the proxy support that makes
77-
// this necessary in the first place (added in 2.38).
78-
return loadedModule;
79-
}
80-
} else if (name === 'stripe') {
81-
stripeReplacement = Object.assign(function () {
75+
fixedModule = loadedModule.defaults({ proxy: false });
76+
fixedModule.INTERCEPTED_BY_HTTPTOOLKIT = true;
77+
} else if (requestedName === 'stripe' && !loadedModule.INTERCEPTED_BY_HTTPTOOLKIT) {
78+
fixedModule = Object.assign(function () {
8279
const result = loadedModule.apply(this, arguments);
8380

8481
if (global.GLOBAL_AGENT) {
@@ -90,28 +87,35 @@ function fixModule(name, loadedModule) {
9087
}
9188

9289
return result;
93-
}, loadedModule);
94-
return stripeReplacement;
90+
}, fixedModule);
91+
fixedModule.INTERCEPTED_BY_HTTPTOOLKIT = true;
92+
}
93+
94+
// Very carefully overwrite node's built-in module cache:
95+
if (fixedModule !== loadedModule && mod._cache[filename] && mod._cache[filename].exports) {
96+
mod._cache[filename].exports = fixedModule;
9597
}
96-
else return loadedModule;
98+
99+
return fixedModule;
97100
}
98101

99102
// Our hook into require():
100-
mod._load = function (name) {
103+
mod._load = function (requestedName, parent, isMain) {
104+
const filename = mod._resolveFilename(requestedName, parent, isMain);
101105
let loadedModule = realLoad.apply(this, arguments);
102106

103107
// Should always be set, but check just in case. This also allows users to disable
104108
// interception explicitly, if need be.
105109
if (!process.env.HTTP_TOOLKIT_ACTIVE) return loadedModule;
106110

107-
// Don't mess with modules if we've seen them before
108-
if (alreadyIntercepted.indexOf(name) >= 0) return loadedModule;
109-
else alreadyIntercepted.push(name);
110-
111111
if (delayedInterception !== false) {
112-
delayedInterception.push({ name: name, loadedModule: loadedModule });
112+
delayedInterception.push({
113+
requestedName: requestedName,
114+
filename: filename,
115+
loadedModule: loadedModule
116+
});
113117
} else {
114-
loadedModule = fixModule(name, loadedModule);
118+
loadedModule = fixModule(requestedName, filename, loadedModule);
115119
}
116120

117121
return loadedModule;

0 commit comments

Comments
 (0)