From 3130497bda3a82da2e964bd50d7e4ab7009aa4c2 Mon Sep 17 00:00:00 2001 From: gengjiawen Date: Wed, 30 Sep 2026 16:28:37 +0800 Subject: [PATCH] fix: use HTTP/1.1 for downloads undici 8 (#3330) negotiates HTTP/2 by default, so since v13.0.1 header and node.lib downloads from nodejs.org use HTTP/2. Since then the parallel install tests on Windows intermittently fail with "TypeError: terminated" (ERR_HTTP2_STREAM_ERROR or UND_ERR_RES_CONTENT_LENGTH_MISMATCH), which RetryAgent does not retry. As a defensive mitigation, go back to HTTP/1.1, as node-gyp used before undici 8, for both the direct and the proxy/custom CA dispatchers. The root cause of the truncated HTTP/2 responses is not established. The HTTPS download tests now use a server that also offers HTTP/2 and assert that HTTP/1.1 was negotiated. --- lib/download.js | 9 +++++++-- test/test-download.js | 22 ++++++++++++++++------ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/lib/download.js b/lib/download.js index dfaca798cf..4be0c9447e 100644 --- a/lib/download.js +++ b/lib/download.js @@ -44,14 +44,19 @@ async function download (gyp, url) { } } +// undici 8 negotiates HTTP/2 by default. Under parallel installs on Windows, +// HTTP/2 downloads from nodejs.org are occasionally cut short ("terminated"), +// which RetryAgent does not retry. Stick to HTTP/1.1 as before undici 8. +const agentOpts = { allowH2: false } + async function createDispatcher (gyp) { const env = process.env const hasProxyEnv = env.http_proxy || env.HTTP_PROXY || env.https_proxy || env.HTTPS_PROXY if (!gyp.opts.proxy && !gyp.opts.cafile && !hasProxyEnv) { - return new RetryAgent(new Agent(), { maxRetries: 3 }) + return new RetryAgent(new Agent(agentOpts), { maxRetries: 3 }) } - const opts = {} + const opts = { ...agentOpts } if (gyp.opts.cafile) { const ca = await readCAFile(gyp.opts.cafile) // EnvHttpProxyAgent forwards opts to both its internal Agent (direct) and diff --git a/test/test-download.js b/test/test-download.js index 24372afeec..79ef5c8bef 100644 --- a/test/test-download.js +++ b/test/test-download.js @@ -5,7 +5,7 @@ const assert = require('assert') const fs = require('fs/promises') const path = require('path') const http = require('http') -const https = require('https') +const http2 = require('http2') const net = require('net') const install = require('../lib/install') const { download, readCAFile } = require('../lib/download') @@ -44,9 +44,12 @@ describe('download', function () { assert.strictEqual(ca.length, 1) - const options = { ca, cert, key } - const server = https.createServer(options, (req, res) => { + // Offers HTTP/2 so the test catches downloads negotiating it + const options = { ca, cert, key, allowHTTP1: true } + let httpVersion + const server = http2.createSecureServer(options, (req, res) => { assert.strictEqual(req.headers['user-agent'], `node-gyp v42 (node ${process.version})`) + httpVersion = req.httpVersion res.end('ok') }) @@ -67,6 +70,7 @@ describe('download', function () { const url = `https://${host}:${port}` const res = await download(gyp, url) assert.strictEqual(await res.text(), 'ok') + assert.strictEqual(httpVersion, '1.1') }) it('download over http with proxy', async function () { @@ -134,11 +138,16 @@ describe('download', function () { const cafile = path.join(__dirname, 'fixtures/ca-proxy.crt') await fs.writeFile(cafile, certs['ca.crt'], 'utf8') - const server = https.createServer({ + let httpVersion + const server = http2.createSecureServer({ ca: await readCAFile(cafile), cert: certs['server.crt'], - key: certs['server.key'] - }, (_, res) => res.end('ok')) + key: certs['server.key'], + allowHTTP1: true + }, (req, res) => { + httpVersion = req.httpVersion + res.end('ok') + }) let proxyUsed = false const pserver = http.createServer() @@ -176,6 +185,7 @@ describe('download', function () { const res = await download(gyp, `https://${host}:${port}`) assert.strictEqual(await res.text(), 'ok') assert.strictEqual(proxyUsed, true) + assert.strictEqual(httpVersion, '1.1') }) it('download over http with noproxy', async function () {