Repository navigation
https: built-in proxy support doesn't send SNI to an https:// proxy #66718
Description
Version
v26.11.1 (also v22.21.1)
Platform
Darwin 25.5.0 arm64. Also seen on Linux in production.
Subsystem
https
What steps will reproduce the bug?
// HTTPS_PROXY=https://localhost:8443 NODE_USE_ENV_PROXY=1 node repro.mjs
import https from 'node:https';
import { createServer } from 'node:net';
// A fake proxy that reads the TLS ClientHello, which carries SNI as plain text
async function check(name, request) {
const proxy = createServer((socket) => {
socket.once('data', (clientHello) => {
console.log(`${name}: proxy received SNI:`, clientHello.includes('localhost'));
socket.destroy();
});
});
await new Promise((resolve) => proxy.listen(8443, 'localhost', resolve));
await request().catch(() => {});
await new Promise((resolve) => proxy.close(resolve));
}
await check('https.get()', () => new Promise((_, reject) => https.get('https://example.com/').on('error', reject)));
await check('fetch()', () => fetch('https://example.com/'));$ HTTPS_PROXY=https://localhost:8443 NODE_USE_ENV_PROXY=1 node repro.mjsThe same happens with new https.Agent({ proxyEnv: { HTTPS_PROXY: 'https://…' } }).
How often does it reproduce? Is there a required condition?
Always, when the proxy URL uses https:// and its host is a name rather than an IP address.
What is the expected behavior? Why is that the expected behavior?
https.get(): proxy received SNI: true
fetch(): proxy received SNI: trueThe TLS connection to an https:// proxy should send the proxy's host name as SNI, just as https.request() does for any host and as fetch() does with the same proxy settings (undici fixed the same problem in nodejs/undici#2939). Servers that pick their certificate by SNI otherwise answer with a default certificate.
What do you see instead?
https.get(): proxy received SNI: false
fetch(): proxy received SNI: trueProxies that pick their certificate by SNI then answer with their default certificate. When that certificate doesn't match the proxy's host name or has expired, every request fails, for example with CERT_HAS_EXPIRED, while fetch() works with the same proxy. Node still verifies the proxy's certificate against its host name, so security isn't affected, only the connection.
Additional information
ProxyConfig builds the options for the proxy connection from only host and port (lib/internal/http.js#L145-L151), and the https agent's createConnection() passes them to tls.connect() unchanged (lib/https.js#L399-L401, #L438). Unlike https.request(), tls.connect() sends no SNI unless servername is set. A possible fix is to set servername for https: proxies whose host isn't an IP address, as undici does.
Unrelated, but nearby: lib/https.js#L211 reads servername: reqOptions.servername || ipType ? undefined : reqOptions.host, which parses as (reqOptions.servername || ipType) ? undefined : reqOptions.host. It's harmless today, because the ...reqOptions spread that follows sets servername again, but it's probably not what was meant.
Activity