Fix "Error: unable to get local issuer certificate" in Node.js HTTPS Requests

intermediate๐Ÿ’š Node.js2026-03-20| Node.js (all versions), Windows/macOS/Linux, corporate networks with SSL inspection, self-signed certificates

Error Message

Error: unable to get local issuer certificate
#nodejs#ssl#https#certificate#tls

The Error

You're making an HTTPS request in Node.js โ€” using fetch, axios, https, or got โ€” and it dies with:

Error: unable to get local issuer certificate
    at TLSSocket.onConnectEnd (_tls_wrap.js:1495:19)
    at Object.onceWrapper (events.js:422:26)
    at TLSSocket.emit (events.js:327:22)

Or one of these variants:

Error: self-signed certificate in certificate chain
Error: certificate has expired
Error: UNABLE_TO_GET_ISSUER_CERT_LOCALLY

The request fails hard. Node's TLS stack looked at the server's certificate, couldn't verify who signed it, and killed the connection.

Root Cause

Node.js ships with its own certificate store โ€” bundled from Mozilla's CA list. It does not use your OS trust store. So when a server presents a certificate signed by a CA that isn't in Node's bundle โ€” a corporate intermediate CA, an internal PKI, or a self-signed cert โ€” Node refuses the handshake.

This catches developers off guard because browsers and curl work fine on the same machine. They use the OS trust store. Node doesn't.

Where this typically bites you:

  • Corporate networks with SSL inspection (the proxy intercepts and re-signs HTTPS traffic with its own root CA)
  • Internal APIs using self-signed or privately-issued certificates
  • Local HTTPS dev environments
  • Expired intermediate certificates somewhere in the chain

Debug First

Don't guess. Run these commands to see exactly what certificate chain the server is presenting:

# Show cert chain details: issuer, subject, and expiry date
openssl s_client -connect your-api-host.com:443 -showcerts 2>/dev/null | openssl x509 -noout -text | grep -E "Issuer|Subject|Not After"
# curl gives a clear, readable error message
curl -v https://your-api-host.com 2>&1 | grep -E "SSL|certificate|issuer"

The output tells you whether you're dealing with a self-signed cert, a missing intermediate, an expired cert, or a corporate proxy intercept. The fix depends on the cause โ€” so check before you patch.

Fix 1 โ€” Add the CA Certificate (Recommended)

Tell Node.js which CA to trust. Get the CA certificate file โ€” ask your IT team, export it from your browser, or pull it from the server โ€” then pass it directly in your request agent:

// Using the https module
const https = require('https');
const fs = require('fs');

const agent = new https.Agent({
  ca: fs.readFileSync('/path/to/ca-certificate.pem')
});

https.get({ hostname: 'your-api-host.com', path: '/', agent }, (res) => {
  console.log('Status:', res.statusCode);
});
// Using axios
const axios = require('axios');
const https = require('https');
const fs = require('fs');

const httpsAgent = new https.Agent({
  ca: fs.readFileSync('/path/to/ca-certificate.pem')
});

const client = axios.create({ httpsAgent });
await client.get('https://your-api-host.com/api/data');
// Using native fetch (Node.js 18+) โ€” native fetch doesn't support the agent option;
// use undici's Agent with a dispatcher instead
import { Agent } from 'undici';
import { readFileSync } from 'fs';

const dispatcher = new Agent({
  connect: {
    ca: readFileSync('/path/to/ca-certificate.pem')
  }
});

fetch('https://your-api-host.com/api/data', { dispatcher });

To export the CA cert from Chrome or Firefox: navigate to the URL โ†’ click the padlock โ†’ View Certificate โ†’ export as PEM format.

On Linux, you can point directly at the system CA bundle instead of a custom file:

const agent = new https.Agent({
  ca: fs.readFileSync('/etc/ssl/certs/ca-certificates.crt')
});

Fix 2 โ€” Set NODE_EXTRA_CA_CERTS (No Code Changes Required)

Can't touch the application code? Use the NODE_EXTRA_CA_CERTS environment variable. Node appends those certificates to its built-in trust store โ€” your existing CAs stay intact, the new one gets added on top. Nothing breaks.

# Linux/macOS
export NODE_EXTRA_CA_CERTS=/path/to/ca-certificate.pem
node your-app.js
# Windows (Command Prompt)
set NODE_EXTRA_CA_CERTS=C:\certs\ca-certificate.pem
node your-app.js
# In .env file (with dotenv)
NODE_EXTRA_CA_CERTS=/path/to/ca-certificate.pem

For corporate environments, drop this into your shell profile (~/.bashrc, ~/.zshrc) or your CI/CD environment variables. One line โ€” every Node process on that machine picks it up automatically.

Fix 3 โ€” Corporate SSL Inspection (Proxy Environment)

Behind a corporate proxy? It's likely intercepting all HTTPS traffic and re-signing it with a corporate root CA. Your browser trusts it because that CA is installed in the OS trust store. Node never looks there.

Export the corporate root CA and point NODE_EXTRA_CA_CERTS at it:

# macOS โ€” export all certs from System keychain in PEM format
security find-certificate -a -p /Library/Keychains/System.keychain > corporate-ca.pem
export NODE_EXTRA_CA_CERTS=corporate-ca.pem
# Ubuntu/Debian โ€” use the system bundle directly
export NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
# Windows โ€” export from cert store (PowerShell)
$certs = Get-ChildItem Cert:\LocalMachine\Root
$certs | ForEach-Object {
    $_ | Export-Certificate -FilePath "$($_.Thumbprint).cer" -Type CERT
}

Fix 4 โ€” Disable Verification (Development Only)

This is the escape hatch. Use it only in local dev โ€” never in staging or production. Disabling TLS verification means any certificate passes, including forged ones from an attacker doing MITM. Security audits will flag this immediately.

// Affects all HTTPS requests in the process
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';

// Or scoped to specific requests with axios
const client = axios.create({
  httpsAgent: new https.Agent({ rejectUnauthorized: false })
});
# Or set inline when launching
NODE_TLS_REJECT_UNAUTHORIZED=0 node your-app.js

At minimum, add a noisy log line so it's obvious when this flag is live:

if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') {
  console.warn('WARNING: TLS verification disabled. Do not use in production!');
}

Verify the Fix

Run this minimal test script to confirm the connection actually works end-to-end:

// test-ssl.js
const https = require('https');
const fs = require('fs');

const options = {
  hostname: 'your-api-host.com',
  port: 443,
  path: '/',
  method: 'GET',
  // Only if using Fix 1:
  // agent: new https.Agent({ ca: fs.readFileSync('/path/to/ca.pem') })
};

const req = https.request(options, (res) => {
  console.log(`SUCCESS: Status ${res.statusCode}`);
});

req.on('error', (e) => {
  console.error(`FAILED: ${e.message}`);
});

req.end();
node test-ssl.js
# Expected: SUCCESS: Status 200

Lessons Learned

  • Reach for NODE_EXTRA_CA_CERTS first โ€” it's secure, requires zero code changes, and works across every library that uses Node's TLS stack.
  • Never disable TLS verification in production โ€” rejectUnauthorized: false is effectively a MITM invitation. Security scanners catch it on the first pass.
  • Node.js ignores the OS trust store โ€” developers coming from browsers or other runtimes are often surprised by this. Docker images and CI pipelines need explicit CA configuration.
  • In Docker, bake the CA into the image: COPY ca.pem /usr/local/share/ca-certificates/ca.crt && update-ca-certificates, then set NODE_EXTRA_CA_CERTS in the entrypoint or environment.
  • If a cert that previously worked suddenly fails, check the expiry date first: openssl s_client -connect host:443 2>/dev/null | openssl x509 -noout -dates. Intermediate certificates expire quietly and often take down multiple services at once.

Related Error Notes