Using Axios with a Custom HTTPS Agent in React and Node.js
Axios accepts an httpsAgent option, an instance of Node's https.Agent, which controls TLS and connection reuse: trusting a private certificate authority, presenting a client certificate, keeping sockets alive. It only works where Axios runs on Node.js. In a React app running in the browser the option is ignored, because the browser owns TLS. This article shows both sides and how to structure a React project so the agent lives where it can have an effect.
The Node examples were run with Axios 1.21 on Node.js 22 against a local HTTPS server with a self-signed certificate.
Why the agent does nothing in the browser
Axios picks an adapter at runtime: on Node.js it uses the http and https modules, in the browser it uses XMLHttpRequest or fetch. Browser APIs expose no way to choose trusted certificates, client keys or socket pools; those are decided by the browser and the operating system. So in client-side React code:
httpsAgentandhttpAgentare silently ignored.import https from "https"either fails at build time or pulls in a useless polyfill, depending on the bundler.- A certificate error ("NET::ERR_CERT_AUTHORITY_INVALID") must be fixed on the server or in the device's trust store, not in JavaScript.
The agent matters wherever React code runs on Node: server-side rendering, framework server functions and API routes, build scripts, and tests run with Node-based runners.
A custom agent on Node.js
The typical case is an internal API whose certificate is signed by a company CA that Node does not trust by default. Without help, the request fails:
import axios from "axios";
try {
await axios.get("https://localhost:8443/health");
} catch (err) {
console.log("default:", err.code);
}
default: DEPTH_ZERO_SELF_SIGNED_CERT
Create one agent that trusts the CA, and attach it to an Axios instance:
import https from "node:https";
import fs from "node:fs";
import axios from "axios";
const httpsAgent = new https.Agent({
ca: fs.readFileSync("ca.pem"), // the private CA (or self-signed cert)
keepAlive: true, // reuse TLS connections
maxSockets: 10, // cap parallel sockets per host
});
const api = axios.create({
baseURL: "https://localhost:8443",
httpsAgent,
timeout: 5000,
});
const { status, data } = await api.get("/health");
console.log("custom agent:", status, data);
custom agent: 200 { ok: true, path: '/health' }
Passing ca replaces Node's default list of trusted roots for this agent, so this instance will no longer reach public HTTPS sites. Use a separate Axios instance for public APIs, or set the NODE_EXTRA_CA_CERTS environment variable to a PEM file, which adds certificates to the defaults for the whole process.
Other useful agent options
| Option | Use |
|---|---|
cert, key (or pfx + passphrase) | Present a client certificate for mutual TLS |
keepAlive, keepAliveMsecs | Reuse connections instead of a TLS handshake per request |
maxSockets, maxFreeSockets | Limit concurrency and idle sockets per host |
servername | Override the SNI name when connecting by IP address |
minVersion | Require, for example, "TLSv1.3" |
Create the agent once per process and share it. A new agent per request discards the connection pool and gives you a full handshake every time. Call httpsAgent.destroy() on shutdown so open keep-alive sockets do not keep the process running. The same "one pool, reused" rule applies in Python: see sending HTTP requests with urllib3.
Never ship rejectUnauthorized: false
The most copied snippet for this problem is:
// Do not use outside a throwaway local experiment
const insecure = new https.Agent({ rejectUnauthorized: false });
It turns off certificate verification, which means anyone on the network path can impersonate the server and read or modify the traffic, including tokens you send. Setting NODE_TLS_REJECT_UNAUTHORIZED=0 does the same for every request in the process, and Node prints a warning for good reason. Trusting the specific CA with ca solves the same error without that risk.
Where to put the agent in a React project
The clean structure is a backend-for-frontend: the browser calls your own server, and your server calls the internal API with the agent.
- Server module. Create the agent and Axios instance in a file that is only imported by server code (an API route, a server action, an Express handler). Many frameworks let you mark such modules as server-only so the bundler refuses to include them in client code.
- Browser code. Calls a same-origin path such as
/api/orderswith plain Axios orfetch, no agent. - Secrets. Client keys, CA bundles and API tokens stay on the server, read from files or environment variables, never in the JavaScript bundle.
// server/internalApi.js (server-only)
import https from "node:https";
import fs from "node:fs";
import axios from "axios";
export const internalApi = axios.create({
baseURL: process.env.INTERNAL_API_URL,
timeout: 5000,
httpsAgent: new https.Agent({
ca: fs.readFileSync(process.env.INTERNAL_CA_PATH),
keepAlive: true,
}),
});
// an API route or server handler
export async function getOrders() {
const { data } = await internalApi.get("/orders");
return data;
}
// client component: no agent, same-origin call
import axios from "axios";
import { useEffect, useState } from "react";
export function Orders() {
const [orders, setOrders] = useState([]);
useEffect(() => {
const controller = new AbortController();
axios.get("/api/orders", { signal: controller.signal })
.then((res) => setOrders(res.data))
.catch((err) => { if (!axios.isCancel(err)) console.error(err); });
return () => controller.abort();
}, []);
return <ul>{orders.map((o) => <li key={o.id}>{o.label}</li>)}</ul>;
}
The AbortController cancels the request if the component unmounts, which avoids state updates on unmounted components and duplicate requests in development mode. This split is the same separation of concerns described in what a CRUD application is: the browser talks to your API, your API talks to everything else.
Proxies and the agent
Axios on Node reads HTTPS_PROXY and NO_PROXY from the environment. When a proxy is in use, the TLS connection to the target goes through a tunnel, and the interaction between the proxy configuration and a custom agent has changed across Axios releases. If requests behave differently behind a corporate proxy, set proxy: false on the instance and use a dedicated proxy agent package, or add the internal host to NO_PROXY.
Troubleshooting
DEPTH_ZERO_SELF_SIGNED_CERTorSELF_SIGNED_CERT_IN_CHAIN: the issuing CA is not trusted; pass it ascaor viaNODE_EXTRA_CA_CERTS.UNABLE_TO_VERIFY_LEAF_SIGNATURE: the server does not send its intermediate certificates; fix the server's chain.ERR_TLS_CERT_ALTNAME_INVALID: the hostname is not in the certificate's subject alternative names; call the right hostname or setservername.- The agent "has no effect": check that the code runs on the server. A
console.login the browser console means it does not.
Summary
An https.Agent passed as httpsAgent controls trusted CAs, client certificates and keep-alive for Axios on Node.js, and is ignored in the browser. Put it in server-only code, create it once, trust specific CAs instead of disabling verification, and let the React client call your own same-origin API.
Sources
- Node.js documentation, https.Agent and tls.connect options.
- React documentation, useEffect.
- Axios documentation, Request Config (
httpAgent,httpsAgent,proxy).
WebHTTP requests with urllib3
How to send GET and POST requests with urllib3: PoolManager, query fields, JSON bodies, headers, timeouts, Retry, redirects, TLS, streaming and errors (v2).
WebWhat is a CRUD application?
CRUD means Create, Read, Update, Delete. See how it maps to SQL and REST HTTP methods, with a runnable Python example, validation, soft deletes and idempotency.
WebScrapy vs Selenium
Scrapy vs Selenium compared: architecture, JavaScript rendering, speed, scaling and pipelines, with working code for each and when to use scrapy-playwright.