This error occurs when the hostname sent during TLS handshake (Server Name Indication) does not match the hostname on the SSL/TLS certificate. The server receives a different SNI name than what the certificate is valid for, causing verification to fail.
The TLS SNI (Server Name Indication) mismatch error happens when the server name sent to the TLS server during the handshake doesn't match the certificate's Subject Alternative Names (SANs) or Common Name (CN). SNI is an extension to the TLS protocol that allows clients to specify which hostname they're connecting to before the server presents its certificate. This is critical for servers hosting multiple TLS certificates (one per domain). When Node.js makes an HTTPS request, it automatically sends the target hostname as the SNI value. If this SNI name doesn't match what the certificate is valid for, the TLS handshake fails. This error differs from a generic certificate validation failure. The certificate itself may be valid (not expired, properly signed), but the hostname it covers doesn't match the one being requested.
When making HTTPS requests with the https module or a client library, always specify the servername option to ensure it matches the certificate's hostname:
const https = require('https');
// Using tls.connect()
const socket = require('tls').connect({
host: '192.168.1.100', // Can be IP address
port: 443,
servername: 'api.example.com', // Must match certificate CN or SAN
ca: [certificateBuffer]
});
socket.on('connect', () => {
console.log('Connected successfully');
socket.end();
});
socket.on('error', (err) => {
console.error('TLS error:', err.message);
});For https.request():
const https = require('https');
https.request({
hostname: '192.168.1.100',
port: 443,
path: '/api/endpoint',
servername: 'api.example.com', // This matches the certificate
method: 'GET'
}, (res) => {
console.log('Success');
}).end();When making requests through an agent, configure the servername properly:
const https = require('https');
const agent = new https.Agent({
hostname: '192.168.1.100',
servername: 'api.example.com', // Ensure this matches certificate
rejectUnauthorized: true,
ca: [certificateBuffer]
});
https.request({
hostname: '192.168.1.100',
agent: agent,
servername: 'api.example.com'
}, (res) => {
console.log('Status:', res.statusCode);
}).end();If using axios or other HTTP libraries:
const axios = require('axios');
const https = require('https');
const agent = new https.Agent({
servername: 'api.example.com',
rejectUnauthorized: true,
ca: [certificateBuffer]
});
axios.get('https://192.168.1.100/api/endpoint', {
httpAgent: agent,
httpsAgent: agent
});Check what hostnames the certificate is valid for:
# View certificate details
openssl x509 -in certificate.pem -text -noout
# Look for the "Subject Alternative Name" section
# Example output:
# Subject Alternative Name:
# DNS:api.example.com
# DNS:api.alt.com
# IP Address:192.168.1.100If the hostname you're connecting to is not in the SAN list or CN, the connection will fail. You have two options:
1. Connect using one of the hostnames that IS on the certificate
2. Get a new certificate that includes your desired hostname
To check from Node.js:
const tls = require('tls');
const fs = require('fs');
const cert = fs.readFileSync('certificate.pem', 'utf8');
const decoded = require('crypto').createSecureContext(cert);
// The cert object will contain SAN information
console.log('Certificate details loaded');If your server is behind an IP address but has a domain certificate, connect to the domain instead:
Bad (will fail SNI validation):
const https = require('https');
https.request({
hostname: '192.168.1.100', // IP address
port: 443,
path: '/api/data',
servername: '192.168.1.100' // Can't match certificate CN/SAN
}, callback).end();Good (proper SNI):
const https = require('https');
https.request({
hostname: 'internal.company.com', // Domain with certificate
port: 443,
path: '/api/data',
servername: 'internal.company.com'
}, callback).end();If you must use an IP address, ensure:
1. The certificate includes the IP address in its SAN
2. Or disable certificate verification only in development (NOT production):
// DEVELOPMENT ONLY - Never do this in production
const agent = new https.Agent({
rejectUnauthorized: false
});When acting as a proxy, don't blindly use the incoming Host header for SNI:
Bad (SNI mismatch when proxying):
const https = require('https');
// Client sends: Host: client.example.com
// But we're forwarding to upstream.internal.com
const hostHeader = req.headers.host; // "client.example.com"
https.request({
hostname: 'upstream.internal.com',
servername: hostHeader, // WRONG! Should be upstream.internal.com
path: req.url
}, callback).end();Good (explicit SNI for upstream):
const https = require('https');
https.request({
hostname: 'upstream.internal.com',
servername: 'upstream.internal.com', // Matches upstream certificate
path: req.url,
headers: {
'Host': req.headers.host // Client's original host
}
}, callback).end();The SNI must match the certificate, while the Host header can be anything you're forwarding.
For advanced scenarios, override certificate validation with custom logic:
const tls = require('tls');
const https = require('https');
// Custom validation function
function checkServerIdentity(servername, cert) {
// Allow specific mismatches in development
if (process.env.NODE_ENV === 'development') {
const allowedHosts = ['localhost', '127.0.0.1', 'internal.local'];
if (allowedHosts.includes(servername)) {
return undefined; // Validation passes
}
}
// Otherwise use default validation
return tls.checkServerIdentity(servername, cert);
}
const agent = new https.Agent({
checkServerIdentity: checkServerIdentity,
servername: 'api.example.com'
});
https.request({
hostname: '192.168.1.100',
agent: agent,
servername: 'api.example.com'
}, callback).end();Important: Only override validation in development environments with trusted certificates.
Verify your SNI setup before testing in Node.js:
# Connect to server with explicit SNI name
openssl s_client -connect 192.168.1.100:443 -servername api.example.com
# You should see:
# Verify return code: 0 (ok)
# subject=CN = api.example.com
# If you get a certificate mismatch, the certificate doesn't cover that hostnameIf openssl succeeds but Node.js fails, check your Node.js code for:
1. Missing servername option
2. Incorrect servername value
3. Old Node.js version without proper TLS support
Test directly in Node.js:
const tls = require('tls');
tls.connect({
host: '192.168.1.100',
port: 443,
servername: 'api.example.com',
rejectUnauthorized: true
}, () => {
console.log('Connected successfully');
process.exit(0);
}).on('error', (err) => {
console.error('TLS Error:', err.message);
process.exit(1);
});Understanding SNI in Modern TLS
SNI (Server Name Indication) is a critical feature for hosting multiple HTTPS sites on a single IP address. Before SNI, each HTTPS site needed its own IP. SNI allows servers to determine which certificate to send based on the hostname sent by the client during the TLS handshake.
SNI is sent unencrypted as part of the TLS Client Hello, so the server knows which certificate to send before the encrypted connection is established. Node.js should automatically handle SNI, but you must ensure you're specifying the correct servername.
Difference Between Host Header and SNI
- SNI: Sent during TLS handshake, determines which certificate the server sends
- Host Header: Sent in HTTP headers after TLS connection is established, determines which virtual host on the server handles the request
These can be different when proxying:
Client → Proxy → Upstream Server
TLS handshake uses servername: upstream.internal.com
HTTP Host header can be: client.example.comCertificate Subject Alternative Names (SANs)
Modern certificates use SANs instead of just the Common Name (CN). A certificate can be valid for multiple hostnames:
Certificate valid for:
- api.example.com (CN)
- api.example.com (SAN)
- api-alt.example.com (SAN)
- *.internal.company (SAN wildcard)Your SNI must match one of these exactly (or match a wildcard).
Wildcard Certificates and SNI
Wildcard certificates like *.example.com will match:
- api.example.com ✓
- auth.example.com ✓
- example.com ✗ (wildcard doesn't match base domain)
SNI with wildcard:
tls.connect({
servername: 'api.example.com', // Matches *.example.com
// ...
});Self-Signed Certificates in Development
For development with self-signed certificates, create one with the correct hostnames:
# Generate self-signed cert with multiple names
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem \
-days 365 -nodes \
-subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,DNS:127.0.0.1,DNS:*.local"
# Use in Node.js
const tls = require('tls');
const fs = require('fs');
tls.createServer({
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem')
}, (socket) => {
socket.write('Hello from TLS server\n');
socket.end();
}).listen(443);HTTPS/TLS Error Categories
Node.js TLS errors include:
- ERR_TLS_CERT_ALTNAME_INVALID: Hostname doesn't match certificate
- ERR_TLS_SNI_NAME_MISMATCH: SNI name doesn't match certificate
- ERR_TLS_CERT_EXPIRED: Certificate expired
- ENOTFOUND: Hostname can't be resolved to IP
- ECONNREFUSED: Connection refused (target not listening)
This error specifically indicates the certificate is valid but the SNI doesn't match its CN/SANs.
Production Considerations
In production:
- Always set rejectUnauthorized: true (the default)
- Use certificates from trusted CAs
- Ensure SNI matches your certificate exactly
- Monitor certificate expiration dates
- Use environment variables for different configurations per environment
Example production setup:
const https = require('https');
const fs = require('fs');
const agent = new https.Agent({
servername: process.env.TLS_SERVERNAME,
rejectUnauthorized: true,
ca: fs.readFileSync(process.env.CA_BUNDLE)
});
https.request({
hostname: process.env.API_HOST,
agent: agent,
servername: process.env.TLS_SERVERNAME
}, callback).end();Set environment variables per deployment:
API_HOST=api.example.com
TLS_SERVERNAME=api.example.com
CA_BUNDLE=/etc/ssl/certs/ca-bundle.crtError: EMFILE: too many open files, watch
EMFILE: fs.watch() limit exceeded
Error: Middleware next() called multiple times (next() invoked twice)
Express middleware next() called multiple times
Error: Worker failed to initialize (worker startup error)
Worker failed to initialize in Node.js
Error: EMFILE: too many open files, open 'file.txt'
EMFILE: too many open files
Error: cluster.fork() failed (cannot create child process)
cluster.fork() failed - Cannot create child process