Skip to content

Commit 96510b3

Browse files
benjamingrsxa
authored andcommitted
module: prefer async/await in https imports
PR-URL: #41950 Fixes: #41950 Reviewed-By: Guy Bedford <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent 8431fb9 commit 96510b3

File tree

2 files changed

+79
-104
lines changed

2 files changed

+79
-104
lines changed
+68-104
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,24 @@
11
'use strict';
22
const {
3-
ArrayPrototypePush,
4-
Promise,
3+
ObjectPrototypeHasOwnProperty,
54
PromisePrototypeThen,
6-
PromiseResolve,
75
SafeMap,
86
StringPrototypeEndsWith,
97
StringPrototypeSlice,
108
StringPrototypeStartsWith,
119
} = primordials;
1210
const {
13-
Buffer: {
14-
concat: BufferConcat
15-
}
11+
Buffer: { concat: BufferConcat },
1612
} = require('buffer');
1713
const {
1814
ERR_NETWORK_IMPORT_DISALLOWED,
1915
ERR_NETWORK_IMPORT_BAD_RESPONSE,
16+
ERR_MODULE_NOT_FOUND,
2017
} = require('internal/errors').codes;
2118
const { URL } = require('internal/url');
2219
const net = require('net');
23-
20+
const { once } = require('events');
21+
const { compose } = require('stream');
2422
/**
2523
* @typedef CacheEntry
2624
* @property {Promise<string> | string} resolvedHREF
@@ -32,6 +30,9 @@ const net = require('net');
3230
* Only for GET requests, other requests would need new Map
3331
* HTTP cache semantics keep diff caches
3432
*
33+
* It caches either the promise or the cache entry since import.meta.url needs
34+
* the value synchronously for the response location after all redirects.
35+
*
3536
* Maps HREF to pending cache entry
3637
* @type {Map<string, Promise<CacheEntry> | CacheEntry>}
3738
*/
@@ -47,23 +48,23 @@ let HTTPSAgent;
4748
function HTTPSGet(url, opts) {
4849
const https = require('https'); // [1]
4950
HTTPSAgent ??= new https.Agent({ // [2]
50-
keepAlive: true
51+
keepAlive: true,
5152
});
5253
return https.get(url, {
5354
agent: HTTPSAgent,
54-
...opts
55+
...opts,
5556
});
5657
}
5758

5859
let HTTPAgent;
5960
function HTTPGet(url, opts) {
6061
const http = require('http'); // [1]
6162
HTTPAgent ??= new http.Agent({ // [2]
62-
keepAlive: true
63+
keepAlive: true,
6364
});
6465
return http.get(url, {
6566
agent: HTTPAgent,
66-
...opts
67+
...opts,
6768
});
6869
}
6970

@@ -98,118 +99,79 @@ function fetchWithRedirects(parsed) {
9899
return existing;
99100
}
100101
const handler = parsed.protocol === 'http:' ? HTTPGet : HTTPSGet;
101-
const result = new Promise((fulfill, reject) => {
102+
const result = (async () => {
102103
const req = handler(parsed, {
103-
headers: {
104-
Accept: '*/*'
105-
}
106-
})
107-
.on('error', reject)
108-
.on('response', (res) => {
109-
function dispose() {
110-
req.destroy();
111-
res.destroy();
112-
}
113-
if (res.statusCode >= 300 && res.statusCode <= 303) {
114-
if (res.headers.location) {
115-
dispose();
116-
try {
117-
const location = new URL(res.headers.location, parsed);
118-
if (location.protocol !== 'http:' &&
119-
location.protocol !== 'https:') {
120-
reject(new ERR_NETWORK_IMPORT_DISALLOWED(
121-
res.headers.location,
122-
parsed.href,
123-
'cannot redirect to non-network location'));
124-
return;
125-
}
126-
return PromisePrototypeThen(
127-
PromiseResolve(fetchWithRedirects(location)),
128-
(entry) => {
129-
cacheForGET.set(parsed.href, entry);
130-
fulfill(entry);
131-
});
132-
} catch (e) {
133-
dispose();
134-
reject(e);
135-
}
104+
headers: { Accept: '*/*' },
105+
});
106+
// Note that `once` is used here to handle `error` and that it hits the
107+
// `finally` on network error/timeout.
108+
const { 0: res } = await once(req, 'response');
109+
try {
110+
const isRedirect = res.statusCode >= 300 && res.statusCode <= 303;
111+
const hasLocation = ObjectPrototypeHasOwnProperty(res.headers, 'location');
112+
if (isRedirect && hasLocation) {
113+
const location = new URL(res.headers.location, parsed);
114+
if (location.protocol !== 'http:' && location.protocol !== 'https:') {
115+
throw new ERR_NETWORK_IMPORT_DISALLOWED(
116+
res.headers.location,
117+
parsed.href,
118+
'cannot redirect to non-network location'
119+
);
136120
}
121+
const entry = await fetchWithRedirects(location);
122+
cacheForGET.set(parsed.href, entry);
123+
return entry;
124+
}
125+
if (res.statusCode === 404) {
126+
const err = new ERR_MODULE_NOT_FOUND(parsed.href, null);
127+
err.message = `Cannot find module '${parsed.href}', HTTP 404`;
128+
throw err;
137129
}
138130
if (res.statusCode > 303 || res.statusCode < 200) {
139-
dispose();
140-
reject(
141-
new ERR_NETWORK_IMPORT_BAD_RESPONSE(
142-
parsed.href,
143-
'HTTP response returned status code of ' + res.statusCode));
144-
return;
131+
throw new ERR_NETWORK_IMPORT_DISALLOWED(
132+
res.headers.location,
133+
parsed.href,
134+
'cannot redirect to non-network location');
145135
}
146136
const { headers } = res;
147137
const contentType = headers['content-type'];
148138
if (!contentType) {
149-
dispose();
150-
reject(new ERR_NETWORK_IMPORT_BAD_RESPONSE(
139+
throw new ERR_NETWORK_IMPORT_BAD_RESPONSE(
151140
parsed.href,
152-
'the \'Content-Type\' header is required'));
153-
return;
141+
"the 'Content-Type' header is required"
142+
);
154143
}
155144
/**
156145
* @type {CacheEntry}
157146
*/
158147
const entry = {
159148
resolvedHREF: parsed.href,
160149
headers: {
161-
'content-type': res.headers['content-type']
150+
'content-type': res.headers['content-type'],
162151
},
163-
body: new Promise((f, r) => {
164-
const buffers = [];
165-
let size = 0;
152+
body: (async () => {
166153
let bodyStream = res;
167-
let onError;
168154
if (res.headers['content-encoding'] === 'br') {
169-
bodyStream = createBrotliDecompress();
170-
onError = function onError(error) {
171-
bodyStream.close();
172-
dispose();
173-
reject(error);
174-
r(error);
175-
};
176-
res.on('error', onError);
177-
res.pipe(bodyStream);
178-
} else if (res.headers['content-encoding'] === 'gzip' ||
179-
res.headers['content-encoding'] === 'deflate') {
180-
bodyStream = createUnzip();
181-
onError = function onError(error) {
182-
bodyStream.close();
183-
dispose();
184-
reject(error);
185-
r(error);
186-
};
187-
res.on('error', onError);
188-
res.pipe(bodyStream);
189-
} else {
190-
onError = function onError(error) {
191-
dispose();
192-
reject(error);
193-
r(error);
194-
};
155+
bodyStream = compose(res, createBrotliDecompress());
156+
} else if (
157+
res.headers['content-encoding'] === 'gzip' ||
158+
res.headers['content-encoding'] === 'deflate'
159+
) {
160+
bodyStream = compose(res, createUnzip());
195161
}
196-
bodyStream.on('error', onError);
197-
bodyStream.on('data', (d) => {
198-
ArrayPrototypePush(buffers, d);
199-
size += d.length;
200-
});
201-
bodyStream.on('end', () => {
202-
const body = entry.body = /** @type {Buffer} */(
203-
BufferConcat(buffers, size)
204-
);
205-
f(body);
206-
});
207-
}),
162+
const buffers = await bodyStream.toArray();
163+
const body = BufferConcat(buffers);
164+
entry.body = body;
165+
return body;
166+
})(),
208167
};
209168
cacheForGET.set(parsed.href, entry);
210-
fulfill(entry);
211-
});
212-
});
169+
await entry.body;
170+
return entry;
171+
} finally {
172+
req.destroy();
173+
}
174+
})();
213175
cacheForGET.set(parsed.href, result);
214176
return result;
215177
}
@@ -226,8 +188,10 @@ allowList.addRange('127.0.0.1', '127.255.255.255');
226188
*/
227189
async function isLocalAddress(hostname) {
228190
try {
229-
if (StringPrototypeStartsWith(hostname, '[') &&
230-
StringPrototypeEndsWith(hostname, ']')) {
191+
if (
192+
StringPrototypeStartsWith(hostname, '[') &&
193+
StringPrototypeEndsWith(hostname, ']')
194+
) {
231195
hostname = StringPrototypeSlice(hostname, 1, -1);
232196
}
233197
const addr = await dnsLookup(hostname, { verbatim: true });
@@ -275,5 +239,5 @@ function fetchModule(parsed, { parentURL }) {
275239
}
276240

277241
module.exports = {
278-
fetchModule: fetchModule
242+
fetchModule: fetchModule,
279243
};

test/es-module/test-http-imports.mjs

+11
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ for (const { protocol, createServer } of [
6464
const server = createServer(function(_req, res) {
6565
const url = new URL(_req.url, host);
6666
const redirect = url.searchParams.get('redirect');
67+
if (url.pathname === '/not-found') {
68+
res.writeHead(404);
69+
res.end();
70+
return;
71+
}
6772
if (redirect) {
6873
const { status, location } = JSON.parse(redirect);
6974
res.writeHead(status, {
@@ -165,6 +170,12 @@ for (const { protocol, createServer } of [
165170
import(unsupportedMIME.href),
166171
{ code: 'ERR_UNKNOWN_MODULE_FORMAT' }
167172
);
173+
const notFound = new URL(url.href);
174+
notFound.pathname = '/not-found';
175+
await assert.rejects(
176+
import(notFound.href),
177+
{ code: 'ERR_MODULE_NOT_FOUND' },
178+
);
168179

169180
server.close();
170181
}

0 commit comments

Comments
 (0)