Axios Proxy Not Working for HTTPS: Causes and Fixes
Axios proxy not working for HTTPS? Fix EPROTO, the 400 plain HTTP request sent to HTTPS port error and hangs with proxy:false and https-proxy-agent. Tested.
Your axios proxy is not working for HTTPS, but the same config fetches http:// URLs without complaint. You get one of three things: write EPROTO ... wrong version number, an HTML page titled 400 The plain HTTP request was sent to HTTPS port, or a request that never returns. All three have specific, separate causes, and most of them are in your axios config rather than the proxy. This page gives the diagnosis order (cheapest check first), the fix for each cause, and one client pattern that behaved the same on every axios version we tested.
The three error signatures
Every result quoted on this page comes from our own lab run on 30 October 2026: Node 24.22.0 on Windows Server 2022, axios 1.21.0, 1.16.1, 1.16.0, 1.12.2 and 0.30.0, https-proxy-agent 9.1.0, socks-proxy-agent 10.1.0, a logging HTTP proxy and a minimal SOCKS5 server on 127.0.0.1, and https://api.ipify.org as the target. The logging proxy recorded the first line of every request it received, which is how we know what axios actually sent.
These are the strings to search your logs for:
| What you see | Most likely cause | Jump to |
|---|---|---|
| `400 The plain HTTP request was sent to HTTPS port` | axios older than 1.16.1 sent the HTTPS request to the proxy in plaintext | [Why HTTPS breaks](#absolute-uri-vs-connect) |
| `write EPROTO ... SSL routines:tls_validate_record_header:wrong version number` | axios is speaking TLS to a proxy port that speaks plain HTTP | [Scheme mismatch](#eproto-scheme-mismatch) |
| `Client network socket disconnected before secure TLS connection was established` (`ECONNRESET`) | Same mismatch, against a proxy that drops the connection instead of answering | [Scheme mismatch](#eproto-scheme-mismatch) |
| Request never settles, `timeout` ignored | Silent proxy, wrong port type, or a tunnel stuck before the socket exists | [Hanging requests](#hanging-requests) |
| `200`, but the IP is your server's own | `adapter: 'fetch'`, `NO_PROXY`, or only `HTTP_PROXY` set | [Environment variables](#env-var-override) |
| `ERR_INVALID_URL Invalid URL` before any network traffic | Unencoded `@` or `#` in the proxy password | [Credentials](#credential-encoding) |
| `Request failed with status code 407` | Wrong credentials, or a password you encoded twice | [Credentials](#credential-encoding) |
Why HTTPS breaks: absolute-URI forwarding vs a CONNECT tunnel
A forward proxy can carry a request in two ways. For plain HTTP, the client sends the full URL in the request line (GET http://host/path HTTP/1.1) and the proxy fetches it. For HTTPS, the client is supposed to send CONNECT host:443, wait for 200 Connection Established, and then run TLS through that raw tunnel straight to the origin. The proxy never sees the URL path, headers or body. The HTTP CONNECT method explainer covers the protocol itself; what matters here is which one axios picks.
For most of its life, axios's built-in proxy option used the plain-HTTP method for everything. Our logging proxy saw this from axios 1.12.2 for an HTTPS target:
GET https://api.ipify.org/?format=json
No CONNECT, and no TLS from your side. What happens next depends entirely on the proxy. A proxy that originates TLS itself can complete the request, though it reads your full URL and headers in cleartext along the way. A proxy that just relays bytes opens TCP to port 443 and forwards the plaintext request, and the target's web server answers with the HTML error page every searcher of this topic has seen:
<html>
<head><title>400 The plain HTTP request was sent to HTTPS port</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
That 400 does not come from the proxy. It comes from the destination, which received an unencrypted request on its TLS port.
The axios team changed this in v1.16.1, published to npm on 13 May 2026. The changelog lists it under security fixes as "Proxy Cleartext Leak: Fixed an issue where HTTPS request data could be transmitted in cleartext to an HTTP proxy under certain configurations" (PR #10858, titled "fix: https data in cleartext to proxy"). From that release, axios bundles https-proxy-agent 5.x as a dependency and opens a CONNECT tunnel for https:// targets. The current request-config docs say so directly: "For https:// targets, axios establishes a CONNECT tunnel through the proxy and performs TLS end-to-end with the origin." Our run confirms the cut-over is exact:
| axios version | Built-in `proxy`, HTTPS target | What the proxy received | Result |
|---|---|---|---|
| 1.12.2 (Sep 2025) | `{ protocol: 'http', host, port }` | `GET https://api.ipify.org/...` | 400 plain HTTP to HTTPS port |
| 1.16.0 (2 May 2026) | same | `GET https://api.ipify.org/...` | 400 plain HTTP to HTTPS port |
| 1.16.1 (13 May 2026) | same | `CONNECT api.ipify.org:443` | 200 |
| 1.21.0 (27 Oct 2026, current latest) | same | `CONNECT api.ipify.org:443` | 200 |
So the first question for any "works on HTTP, fails on HTTPS" report is not about the proxy at all. It's which axios version is installed, and that includes copies pulled in by other packages.
Scraping at scale? Skip the blocks.
Fast, unblockable datacentre proxies with unlimited bandwidth.
What to check first, in order
Work down this list and stop at the first check that fails. Each one takes seconds.
- Installed axios version.
npm ls axiosshows every copy in the tree, including the one an SDK bundles. Anything below 1.16.1 has the plaintext behaviour above. - Proxy variables in the environment. axios reads
HTTPS_PROXY(viaproxy-from-env) wheneverproxyis not set tofalse, and it will quietly take priority over an agent you pass in. - Adapter. If the code or a shared instance sets
adapter: 'fetch', neitherproxynorhttpsAgentdoes anything. - Scheme and port pairing.
http://URL to an HTTP proxy port,socks5h://URL to a SOCKS port. A mismatch produces EPROTO or a hang. - Credentials. Special characters must be percent-encoded in a URL and must not be encoded in an
authobject. - The proxy itself. Only now compare with curl. If curl fails with the same credentials, the problem is upstream of axios.
The first three checks are one-liners:
npm ls axios
node -e "console.log(require('axios/package.json').version)"
node -e "for (const k of ['HTTPS_PROXY','https_proxy','HTTP_PROXY','http_proxy','NO_PROXY','no_proxy']) if (process.env[k]) console.log(k, '=', process.env[k])"
grep -rn "adapter" src/ | grep -i fetch
And the curl comparison for step 6, using SparkProxy's rotating gateway port:
curl -sS -x "http://USER:PASS@gateway.sparkproxy.io:11000" https://api.ipify.org/?format=json
curl uses CONNECT for HTTPS through an http:// proxy, so a success here and a failure in Node points squarely at the axios config. The same split is how the proxy error codes reference separates proxy-side status codes from client-side ones.
The pattern that works on every version: proxy:false plus an agent
Upgrading to 1.16.1 or later fixes the built-in path. You often can't control that, though: a transitive dependency pins an older axios, or a deployment has HTTPS_PROXY set by the platform. The pattern below returned 200 through our proxy on 1.12.2, 1.16.0, 1.16.1 and 1.21.0 alike, because axios hands the socket to the agent and stays out of tunnelling entirely.
npm install axios https-proxy-agent
import axios from 'axios';
import { HttpsProxyAgent } from 'https-proxy-agent';
const agent = new HttpsProxyAgent('http://USER:PASS@gateway.sparkproxy.io:11000');
const res = await axios.get('https://api.ipify.org/?format=json', {
httpsAgent: agent,
proxy: false, // ignore config.proxy AND HTTPS_PROXY/HTTP_PROXY; let the agent tunnel
});
console.log(res.data); // { ip: '<exit IP>' }
Three details matter.
The proxy URL starts with http:// even though the target is HTTPS. That scheme describes the hop from you to the proxy, which on port 11000 is plain HTTP; TLS to the origin runs inside the tunnel. https-proxy-agent always sends CONNECT, so it is the right agent for HTTPS targets. Our proxy logged CONNECT api.ipify.org:443 for every axios version.
proxy: false is what makes the result independent of the environment. The axios docs describe it as "Use false to disable proxies, ignoring environment variables." Without it, the agent only wins when no proxy variable is set (next section).
https-proxy-agent 9.1.0 (published 8 June 2026) is ESM-only and declares "engines": { "node": ">= 20" }. require('https-proxy-agent') still worked in CommonJS on Node 24.22.0 in our run, because current Node can require() ES modules. On an older runtime, pin a release that supports it or switch the file to ESM.
The Node.js proxy rotation guide builds pools, weighting and retries on top of exactly this agent setup, so this page doesn't repeat that.
EPROTO "wrong version number": a scheme mismatch
This one has nothing to do with axios's version. It appears when the client opens a TLS handshake to a proxy port that expects plain HTTP. The proxy reads the TLS ClientHello as garbage and answers with a plaintext HTTP/1.1 400, and OpenSSL, expecting a TLS record, reports:
write EPROTO 745D0100:error:0A00010B:SSL routines:tls_validate_record_header:wrong version number:openssl\ssl\record\methods\tlsany_meth.c:78:
We got that exact error from axios 1.21.0 and 1.12.2 with either of these configs against a plain HTTP proxy:
// Both are wrong for a plain HTTP proxy port such as 11000
axios.get(url, { proxy: { protocol: 'https', host: 'gateway.sparkproxy.io', port: 11000 } });
axios.get(url, { httpsAgent: new HttpsProxyAgent('https://gateway.sparkproxy.io:11000'), proxy: false });
If the proxy closes the socket instead of answering, the same mistake surfaces as ECONNRESET Client network socket disconnected before secure TLS connection was established.
One version-specific trap sits here too. On axios 1.12.2 and 1.16.0, leaving the protocol field out of the proxy object made axios keep the target's https: scheme for the proxy hop, so it produced the TLS-to-plain-proxy failure. On 1.16.1 and 1.21.0 a missing protocol defaults to http: and the same object worked. Always write protocol: 'http' explicitly and the question goes away.
The fix is to match the scheme to what the port actually speaks. Only use https:// for the proxy URL when the provider documents a TLS listener on that port.
Environment variables that override your agent
This is the most confusing failure because your code looks correct. We ran two proxies, A set in HTTPS_PROXY and B passed as httpsAgent, with no proxy: false:
| axios version | `HTTPS_PROXY`=A, `httpsAgent`=B, no `proxy: false` | Same, with `proxy: false` |
|---|---|---|
| 1.12.2 | A received a plaintext `GET https://...`, result 400 | B received `CONNECT`, 200 |
| 1.16.0 | A received a plaintext `GET https://...`, result 400 | B received `CONNECT`, 200 |
| 1.16.1 | A received `CONNECT`, 200; B received nothing | B received `CONNECT`, 200 |
| 1.21.0 | A received `CONNECT`, 200; B received nothing | B received `CONNECT`, 200 |
On new versions the request succeeds, which hides the problem: your agent, with its credentials, country routing or Sticky Sessions port, is silently skipped in favour of whatever the platform exported. The reason sits in axios's HTTP adapter. It leaves tunnelling alone only when httpsAgent instanceof HttpsProxyAgent is true, and that check runs against the https-proxy-agent 5.x class axios bundles. An agent built from your own https-proxy-agent 9.x install is a different class, fails the check, and gets replaced.
Three more environment behaviours we measured on 1.21.0 and 1.12.2:
HTTP_PROXYalone does not cover HTTPS targets. With onlyHTTP_PROXYset, anhttps://request went direct and the proxy logged nothing. axios resolveshttps_proxyfor HTTPS URLs andhttp_proxyfor HTTP URLs.NO_PROXYwins. WithHTTPS_PROXYset andNO_PROXY=api.ipify.org, the request went direct. A broad entry such as.ioor*can switch the proxy off for half your targets.- The fetch adapter ignores both mechanisms.
adapter: 'fetch'with aproxyobject, or withhttpsAgent, returned 200 from our real IP and the proxy saw zero connections. No error, no warning.
The current axios docs add a fourth player. They state that on Node versions with native environment proxy support, axios "defers environment proxy handling to Node" when the agent has proxyEnv enabled, which includes processes started with NODE_USE_ENV_PROXY=1 or --use-env-proxy. An explicit proxy object is still handled by axios. If you want one rule for all of this, it's proxy: false plus an explicit agent.
Usernames and passwords with @, : or #
Proxy passwords generated by dashboards often contain URL-reserved characters. The two places credentials can go need opposite treatment, and we saw both mistakes fail:
| Where credentials go | Password `p@ss:w#rd` passed as | Result in our run |
|---|---|---|
| Agent URL | raw: `http://user-1:p@ss:w#rd@host:port` | `ERR_INVALID_URL Invalid URL`, nothing sent |
| Agent URL | `encodeURIComponent(password)` | 200, proxy received `user-1:p@ss:w#rd` |
| axios `proxy.auth` object | raw: `{ username: 'user-1', password: 'p@ss:w#rd' }` | 200 on 1.16.1 and 1.21.0 |
| axios `proxy.auth` object | `encodeURIComponent(password)` | 407, proxy received `p%40ss%3Aw%23rd` literally |
The rule: encode inside a URL, never inside the auth object. The object goes straight into the Proxy-Authorization: Basic header, so an encoded value is sent as the literal percent-escaped string and the proxy rejects it.
const user = encodeURIComponent(process.env.SPARKPROXY_USER);
const pass = encodeURIComponent(process.env.SPARKPROXY_PASS);
const agent = new HttpsProxyAgent(`http://${user}:${pass}@gateway.sparkproxy.io:11000`);
A 407 through the agent surfaced in axios as Request failed with status code 407 with error.response.status === 407, the same shape as a target's 4xx. Check error.response.status before assuming the target blocked you. If you'd rather not handle credentials at all, SparkProxy also supports IP whitelist authentication, where the gateway authorises your server's IP and the proxy URL carries no username or password.
SOCKS5 with socks-proxy-agent
axios's built-in proxy object does not speak SOCKS. Setting protocol: 'socks5' failed immediately with ERR_ASSERTION protocol mismatch on 1.12.2. On 1.21.0 it was worse: axios sent an HTTP CONNECT to the SOCKS port, the server rejected the first byte (0x43, the letter C), and the request hung until our 8-second abort fired. Use an agent instead:
npm install socks-proxy-agent
import axios from 'axios';
import { SocksProxyAgent } from 'socks-proxy-agent';
const socks = new SocksProxyAgent('socks5h://USER:PASS@gateway.sparkproxy.io:13000');
const res = await axios.get('https://api.ipify.org/?format=json', {
httpAgent: socks,
httpsAgent: socks,
proxy: false,
});
The h in socks5h:// matters more than it looks. Our SOCKS5 server logs the address type of each request. With socks5://, socks-proxy-agent 10.1.0 resolved the hostname locally and sent an IPv4 address (atyp=1 104.26.12.205:443). With socks5h://, it sent the hostname (atyp=3 api.ipify.org:443) and let the proxy resolve it. Local resolution means your own resolver sees every target hostname, and a geo-targeted exit may connect to an address picked for your location instead of the exit's. Prefer socks5h://. The SOCKS5 setup guide for Python, Node.js and browsers covers the protocol in other clients.
Requests that hang: why timeout does not fire
axios's timeout defaults to 0, meaning no timeout, so any stuck tunnel hangs forever by default. Setting it did not always help. Against a proxy that accepts TCP and then says nothing:
| Config against a silent proxy | axios 1.12.2 | axios 1.21.0 |
|---|---|---|
| Agent + `timeout: 3000` | did not fire (cut by our 8 s abort) | did not fire (8 s abort) |
| Agent + `timeout: 3000` + `maxRedirects: 0` | did not fire | `ECONNABORTED timeout of 3000ms exceeded` at 3.0 s |
| Built-in `proxy` + `timeout: 3000` | `ECONNABORTED` at 3.0 s | did not fire (8 s abort) |
| Agent + `signal: AbortSignal.timeout(3000)` | `ERR_CANCELED` at 3.0 s | `ERR_CANCELED` at 3.0 s |
The maxRedirects: 0 row lines up with the v1.16.0 changelog, which says axios "honoured the timeout option during the connect phase when redirects are disabled". With redirects on (the default), the request runs through follow-redirects and the timer does not cover a tunnel that never produces a socket. An AbortSignal was the only option that fired on time in every combination, so use it as the outer deadline:
try {
await axios.get(url, { httpsAgent: agent, proxy: false, signal: AbortSignal.timeout(20_000) });
} catch (err) {
if (err.code === 'ERR_CANCELED') console.error('proxy or target did not answer within 20 s');
else throw err;
}
The other common hang is a port mix-up: pointing an HTTP proxy config at a SOCKS port. axios 1.12.2 failed fast with ECONNRESET socket hang up; 1.21.0 hung until the abort. On SparkProxy, 11000 and 11002 speak HTTP and 13000 speaks SOCKS5.
A drop-in axios client for SparkProxy gateways
Everything above fits in one factory. It encodes credentials, picks the right agent for the scheme, disables axios's own proxy handling, and puts an abort deadline on every request:
// sparkproxy-axios.mjs
import axios from 'axios';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { SocksProxyAgent } from 'socks-proxy-agent';
const USER = encodeURIComponent(process.env.SPARKPROXY_USER);
const PASS = encodeURIComponent(process.env.SPARKPROXY_PASS);
const ENDPOINTS = {
rotating: `http://${USER}:${PASS}@gateway.sparkproxy.io:11000`, // Random Rotation
sticky: `http://${USER}:${PASS}@gateway.sparkproxy.io:11002`, // Sticky Sessions (5 min)
socks5: `socks5h://${USER}:${PASS}@gateway.sparkproxy.io:13000`,
};
export function proxiedClient(mode = 'rotating', { timeoutMs = 20000 } = {}) {
const url = ENDPOINTS[mode];
const agent = url.startsWith('socks') ? new SocksProxyAgent(url) : new HttpsProxyAgent(url);
const client = axios.create({ httpAgent: agent, httpsAgent: agent, proxy: false });
client.interceptors.request.use((config) => {
config.signal ??= AbortSignal.timeout(timeoutMs);
return config;
});
return client;
}
We ran this code against our lab proxies with only the host and ports swapped, a password containing @ and #, and a deliberately broken HTTPS_PROXY exported in the same process. Both the HTTP and the SOCKS5 client returned 200 for http:// and https:// targets, the proxies received the decoded credentials, and the bogus environment variable was ignored. An unroutable target produced ERR_CANCELED after 2.0 s with timeoutMs: 2000.
Choose the endpoint by the job. Random Rotation on port 11000 suits independent page fetches where each request can leave from a different IP. Sticky Sessions on 11002 keep the same exit for 5 minutes, which is what a login, a cart or a paginated crawl needs; the sticky session explainer covers when that matters. SparkProxy plans start from $75/month (USD, per the sparkproxy.io pricing read on 9 October 2026), are sold as packs rather than per-GB, and include unlimited bandwidth, so retries after a timeout don't add traffic charges. The 24-hour trial is free and asks for no card, which is enough to run the curl check and this client against your real targets.
Failures a proxy will not solve
Once the tunnel is correct, some HTTPS failures remain, and no proxy setting changes them:
- TLS and header fingerprinting. The exit IP changes, but Node's TLS ClientHello and axios's default headers do not. Sites that score clients on those (see what TLS fingerprinting is) will block a perfect proxy setup.
- HTTP/2. The axios docs state that requests with
httpVersion: 2"cannot be proxied": they ignore proxy environment variables, and an explicitproxyobject rejects withERR_NOT_SUPPORT. - Certificate errors from an intercepting corporate proxy.
unable to get local issuer certificatebehind a company gateway is a trust-store problem; give the agent the rightca, neverrejectUnauthorized: falsein production. - Pages that need JavaScript. axios returns the initial HTML. If the data is rendered client-side, routing the request elsewhere doesn't change what comes back. The Cheerio and Node.js scraping guide shows how to check that in a minute.
For those cases, the SparkProxy Scraping API removes the proxy config from your code entirely, because the request goes to a normal HTTPS endpoint:
const res = await axios.get('https://scrape.sparkproxy.io/api/v1', {
headers: { 'X-API-Key': process.env.SPARKPROXY_API_KEY },
params: { url: 'https://www.sparkproxy.io/pricing', render_js: false },
signal: AbortSignal.timeout(60_000),
});
render_js=false is a plain fetch at 1 credit; leaving render_js at its default of true runs a headless browser for 5 credits. New accounts get 1,000 free credits. If you add json_response=true for the envelope (job_id, status_code, duration_ms, credits_used, body), remember that body is base64 and must be decoded before you match anything in it.
Frequently asked questions
FAQ
Because axios versions before 1.16.1 send HTTPS requests to the proxy as a plaintext GET https://... instead of opening a CONNECT tunnel. A proxy that relays it unchanged delivers plaintext to port 443, and the target answers 400 The plain HTTP request was sent to HTTPS port. Upgrade, or use proxy: false with https-proxy-agent.
Version 1.16.1, published 13 May 2026, listed in the changelog as the "Proxy Cleartext Leak" fix (PR #10858). In our tests 1.16.0 still sent plaintext and 1.16.1 sent CONNECT host:443.
Yes, if any proxy environment variable might be set. Without proxy: false, axios 1.21.0 routed our request through HTTPS_PROXY and skipped the https-proxy-agent 9.x agent we passed, because it only recognises its own bundled 5.x class.
Percent-encode it with encodeURIComponent when it goes inside a proxy URL, and pass it raw when it goes in axios's proxy.auth object. Encoding inside the object sends the escaped string literally and produces a 407.
Not through its built-in proxy option. Pass a SocksProxyAgent as both httpAgent and httpsAgent with proxy: false, and use a socks5h:// URL so the proxy, not your machine, resolves the target hostname.
axios's timeout did not cover a stalled proxy tunnel in most of our test combinations unless maxRedirects: 0 was also set. Pass signal: AbortSignal.timeout(ms) instead; it cancelled on time with every version and config we tried.
Get 20% off your first month
Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.
Save up to 15% more on quarterly, half-yearly and yearly plans
Related articles
http_proxy Not Working With sudo, cron and systemd
http_proxy not working with sudo, cron or systemd? Dump the job's real environment, fix env_keep, crontab lines and Environment=, then prove the exit IP.
Website Returning Fake Data to Scraper? Catch Soft Blocks
Website returning fake data to scraper? Catch poisoned prices, fake out-of-stock and truncated results with self-consistency checks and cross-IP votes.
Scrapy: Could Not Open CONNECT Tunnel With Proxy (Fixed)
Scrapy could not open CONNECT tunnel with proxy? Read the status inside the TunnelError, then fix 407s, dropped Proxy-Authorization, timeouts and concurrency.
