Skip to content

Commit 8dd155c

Browse files
feat: replace make-fetch-happen with built-in fetch
Use Node's built-in fetch for downloading headers/tarballs, with undici's EnvHttpProxyAgent providing --proxy, --noproxy and --cafile support (plus http_proxy/https_proxy/no_proxy env var handling). Drops 36 transitive dependencies.
1 parent 19da158 commit 8dd155c

3 files changed

Lines changed: 69 additions & 17 deletions

File tree

lib/download.js

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
const fetch = require('make-fetch-happen')
1+
const { Readable } = require('stream')
2+
const { EnvHttpProxyAgent } = require('undici')
23
const { promises: fs } = require('graceful-fs')
34
const log = require('./log')
45

@@ -10,19 +11,58 @@ async function download (gyp, url) {
1011
'User-Agent': `node-gyp v${gyp.version} (node ${process.version})`,
1112
Connection: 'keep-alive'
1213
},
13-
proxy: gyp.opts.proxy,
14-
noProxy: gyp.opts.noproxy
14+
dispatcher: await createDispatcher(gyp)
1515
}
1616

17-
const cafile = gyp.opts.cafile
18-
if (cafile) {
19-
requestOpts.ca = await readCAFile(cafile)
17+
let res
18+
try {
19+
res = await fetch(url, requestOpts)
20+
} catch (err) {
21+
// Built-in fetch wraps low-level errors in "TypeError: fetch failed" with
22+
// the underlying error on .cause. Callers inspect .code (e.g. ENOTFOUND).
23+
if (err.cause) {
24+
throw err.cause
25+
}
26+
throw err
2027
}
2128

22-
const res = await fetch(url, requestOpts)
2329
log.http(res.status, res.url)
2430

25-
return res
31+
const body = Readable.fromWeb(res.body)
32+
return {
33+
status: res.status,
34+
url: res.url,
35+
body,
36+
text: async () => {
37+
let data = ''
38+
body.setEncoding('utf8')
39+
for await (const chunk of body) {
40+
data += chunk
41+
}
42+
return data
43+
}
44+
}
45+
}
46+
47+
async function createDispatcher (gyp) {
48+
const env = process.env
49+
const hasProxyEnv = env.http_proxy || env.HTTP_PROXY || env.https_proxy || env.HTTPS_PROXY
50+
if (!gyp.opts.proxy && !gyp.opts.cafile && !hasProxyEnv) {
51+
return undefined
52+
}
53+
54+
const opts = {}
55+
if (gyp.opts.cafile) {
56+
opts.connect = { ca: await readCAFile(gyp.opts.cafile) }
57+
}
58+
if (gyp.opts.proxy) {
59+
opts.httpProxy = gyp.opts.proxy
60+
opts.httpsProxy = gyp.opts.proxy
61+
}
62+
if (gyp.opts.noproxy) {
63+
opts.noProxy = gyp.opts.noproxy
64+
}
65+
return new EnvHttpProxyAgent(opts)
2666
}
2767

2868
async function readCAFile (filename) {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@
2525
"env-paths": "^2.2.0",
2626
"exponential-backoff": "^3.1.1",
2727
"graceful-fs": "^4.2.6",
28-
"make-fetch-happen": "^15.0.0",
2928
"nopt": "^9.0.0",
3029
"proc-log": "^6.0.0",
3130
"semver": "^7.3.5",
3231
"tar": "^7.5.4",
3332
"tinyglobby": "^0.2.12",
33+
"undici": "^6.25.0",
3434
"which": "^6.0.0"
3535
},
3636
"engines": {

test/test-download.js

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const fs = require('fs/promises')
66
const path = require('path')
77
const http = require('http')
88
const https = require('https')
9+
const net = require('net')
910
const install = require('../lib/install')
1011
const { download, readCAFile } = require('../lib/download')
1112
const { FULL_TEST, devDir, platformTimeout } = require('./common')
@@ -69,13 +70,22 @@ describe('download', function () {
6970
})
7071

7172
it('download over http with proxy', async function () {
72-
const server = http.createServer((_, res) => {
73+
const server = http.createServer((req, res) => {
74+
assert.strictEqual(req.headers['user-agent'], `node-gyp v42 (node ${process.version})`)
7375
res.end('ok')
7476
})
7577

76-
const pserver = http.createServer((req, res) => {
77-
assert.strictEqual(req.headers['user-agent'], `node-gyp v42 (node ${process.version})`)
78-
res.end('proxy ok')
78+
let proxyUsed = false
79+
const pserver = http.createServer()
80+
pserver.on('connect', (req, clientSocket, head) => {
81+
proxyUsed = true
82+
const [targetHost, targetPort] = req.url.split(':')
83+
const serverSocket = net.connect(targetPort, targetHost, () => {
84+
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n')
85+
serverSocket.write(head)
86+
serverSocket.pipe(clientSocket)
87+
clientSocket.pipe(serverSocket)
88+
})
7989
})
8090

8191
after(() => Promise.all([
@@ -96,7 +106,8 @@ describe('download', function () {
96106
}
97107
const url = `http://${host}:${port}`
98108
const res = await download(gyp, url)
99-
assert.strictEqual(await res.text(), 'proxy ok')
109+
assert.strictEqual(await res.text(), 'ok')
110+
assert.strictEqual(proxyUsed, true)
100111
})
101112

102113
it('download over http with noproxy', async function () {
@@ -105,9 +116,9 @@ describe('download', function () {
105116
res.end('ok')
106117
})
107118

108-
const pserver = http.createServer((_, res) => {
109-
res.end('proxy ok')
110-
})
119+
let proxyUsed = false
120+
const pserver = http.createServer()
121+
pserver.on('connect', () => { proxyUsed = true })
111122

112123
after(() => Promise.all([
113124
new Promise((resolve) => server.close(resolve)),
@@ -128,6 +139,7 @@ describe('download', function () {
128139
const url = `http://${host}:${port}`
129140
const res = await download(gyp, url)
130141
assert.strictEqual(await res.text(), 'ok')
142+
assert.strictEqual(proxyUsed, false)
131143
})
132144

133145
it('download with missing cafile', async function () {

0 commit comments

Comments
 (0)