Should HTTPS_PROXY start with http:// or https://?
Almost always http://. The scheme says how to reach the proxy, not what it carries. What 12 clients do with each form, the exact errors, and how to check.
Key points
- Use
HTTPS_PROXY=http://proxy:portfor an ordinary corporate or Squid proxy. The variable name means "the proxy for HTTPS URLs"; the scheme in its value says how the client talks to the proxy, and the origin's TLS still runs end to end inside theCONNECTtunnel. https://in the value tells the client to open TLS to the proxy itself. Against a plain proxy port that fails in every client that honours it: the proxy answers the ClientHello withHTTP/1.1 400, and the bytes48 54 54("HTT") are read as a TLS record header, hence wrong version number.- The wrong answer survives because some clients ignore the scheme: GNU wget 1.25.0 and CPython's
urllibsent plain HTTP to a proxy written ashttps://and succeeded, and urllib3 did the same until 1.26.0 (2020-11-10). The same line breaks curl, requests, pip, httpx, Node, npm, Go, git and apt. - Use
https://only when the proxy listens with TLS (a PACHTTPS host:portentry, Squidhttps_port). Then the proxy's CA must be trusted by the client's proxy TLS settings:--proxy-cacertin curl,http.proxySSLCAInfoin git,REQUESTS_CA_BUNDLEin requests, which ignores the OS store. - A value with no scheme is not portable: curl, Go, git, httpx and wget assume
http://, but requests and pip reject it asInvalidProxyURL, Node 24 withNODE_USE_ENV_PROXY=1refuses to start, npm givesEINVALIDPROXYand apt reportsUnsupported proxy configured.
Start HTTPS_PROXY with http:// unless you know the proxy itself speaks TLS. The variable name means "the proxy to use for https:// URLs"; the scheme inside its value says how the client reaches the proxy. An ordinary corporate proxy, Squid's http_port included, takes a plain-text CONNECT example.com:443 and then relays the bytes of a TLS session that runs end to end between the client and the origin, so HTTPS_PROXY=http://proxy.corp:3128 does not downgrade anything. Writing https://proxy.corp:3128 instead tells the client to open a second TLS session to the proxy, and against a plain proxy port that fails with wrong version number in curl, requests, pip, httpx, Node, npm, Go, git and apt.
Both answers circulate because both have "worked" for someone. A handful of clients ignore the scheme and speak plain HTTP to the proxy whatever you write, so a copied https:// line runs fine in one tool and breaks the next. The table below is what each client did with each form of the value, tested on 17 October 2026.
| You see | Cause | Fix |
|---|---|---|
wrong version number, WRONG_VERSION_NUMBER, EPROTO, first record does not look like a TLS handshake, An unexpected TLS packet was received | HTTPS_PROXY=https://... pointing at a plain HTTP proxy port | Change the value to http:// with the same host and port |
Your proxy appears to only use HTTP and not HTTPS, try changing your proxy URL to be HTTP (requests, urllib3 1.26.8 and later) | Same | Same |
unable to get local issuer certificate or x509: certificate signed by unknown authority with a proxy that really is TLS | The proxy's own certificate is not trusted by the client's proxy TLS settings | Add the proxy's CA where that client looks for it (table further down) |
InvalidProxyURL, EINVALIDPROXY, Invalid URL protocol, Unsupported proxy configured: host://port | No scheme in the value | Add http:// |
Works in wget or a Python script using urllib, fails in curl, pip and npm on the same shell | https:// value; those two clients ignore the scheme | Change the value to http://; the clients that "worked" keep working |
How the matrix was tested#
The proxy was Squid 6.12 on Alpine 3.22, listening for plain HTTP on port 8888 and allowing CONNECT to port 443 only. A second endpoint on port 3129 was a real TLS proxy: socat terminating TLS with a certificate for the name tlsproxy, issued by a private test CA, and relaying to the same Squid. Every client fetched https://example.com/ with HTTPS_PROXY (and https_proxy) set to four values: http://plainproxy:8888, https://plainproxy:8888, https://tlsproxy:3129 and the scheme-less plainproxy:8888. Squid's access log was counted before and after every request, which is how the clients that silently ignored the variable were caught.
Versions: curl 8.14.1 (OpenSSL 3.5), GNU Wget 1.25.0 and BusyBox 1.37.0 on Alpine 3.22; Python 3.13.16 with requests 2.34.2, urllib3 2.8.0, httpx 0.28.1 and pip 26.2.1; Node 24.22.0 and npm 11.19.1; Go 1.25.14; git 2.47.3 and apt 3.0.3 on Debian 13. "TLS proxy" results below are with the test CA made trustworthy for that client in the way stated; without that, every client that supports TLS proxies failed certificate verification, as it should.
What 12 clients did with each value#
| Client | http://proxy:port | https:// to a plain proxy | https:// to a TLS proxy | No scheme |
|---|---|---|---|---|
| curl 8.14.1 | Works | Fails: curl: (35) TLS connect error: error:0A00010B:SSL routines::wrong version number | Works (OS store or --proxy-cacert) | Works, http:// assumed |
| GNU Wget 1.25.0 | Works | Works: speaks plain HTTP to the proxy anyway | Fails: Failed reading proxy response: Connection reset by peer | Works, prints Prepended http:// to 'plainproxy:8888' |
| BusyBox wget 1.37.0 | Not read: connected direct | Not read | Not read | Not read |
| Python requests 2.34.2 | Works | Fails: ProxyError ... Your proxy appears to only use HTTP and not HTTPS | Works with REQUESTS_CA_BUNDLE or verify=; not with the CA in the OS store | Fails: InvalidProxyURL: Please check proxy URL. It is malformed and could be missing the host. |
| pip 26.2.1 | Works | Fails: SSLError(1, '[SSL: WRONG_VERSION_NUMBER] wrong version number'), then No matching distribution found | Works (OS store or --cert) | Fails: InvalidProxyURL |
| httpx 0.28.1 | Works | Fails: ConnectError: [SSL: WRONG_VERSION_NUMBER] | Works (OS store or SSL_CERT_FILE) | Works, http:// assumed |
CPython urllib.request | Works | Works: speaks plain HTTP to the proxy anyway | Fails: [Errno 104] Connection reset by peer | Works |
Node 24.22.0 fetch and https | Works only with NODE_USE_ENV_PROXY=1 | Fails: ERR_SSL_WRONG_VERSION_NUMBER | Works with NODE_EXTRA_CA_CERTS | Process exits at startup: Invalid URL protocol: the URL must start with http: or https: |
| npm 11.19.1 | Works | Fails: request to https://registry.npmjs.org/... failed, reason: write EPROTO ... wrong version number | Works with NODE_EXTRA_CA_CERTS | Fails: EINVALIDPROXY Invalid protocol plainproxy: |
Go 1.25.14 net/http | Works | Fails: proxyconnect tcp: tls: first record does not look like a TLS handshake | Works (OS store) | Works, http:// assumed (documented) |
| git 2.47.3 (Debian, GnuTLS) | Works | Fails: GnuTLS, handshake failed: An unexpected TLS packet was received. | Works (OS store, http.proxySSLCAInfo or GIT_PROXY_SSL_CAINFO) | Works |
| apt 3.0.3 | Works | Fails: SSL connection failed: error:0A00010B:SSL routines::wrong version number | Works (OS store) | Fails: Unsupported proxy configured: plainproxy://8888 |
Three rows deserve a note before anything else.
BusyBox wget does not read https_proxy at all. With the variable pointing at an unresolvable host it still downloaded the page, directly, and Squid logged nothing. Set http_proxy instead and BusyBox wget sends the proxy an absolute-form GET https://example.com/ rather than a CONNECT; Squid logged it as TCP_MISS_ABORTED/000 and wget printed error getting response: Invalid argument. On Alpine-based images, install GNU wget or curl if the build has to go through a proxy.
Node ignores the variables unless asked. Without NODE_USE_ENV_PROXY=1 (or --use-env-proxy), every fetch and https.get above connected directly and Squid saw nothing, whatever the value. The Node documentation dates the environment variable to v24.0.0 and the http/https agent support to v24.5.0. npm is different: it reads the proxy variables into its own configuration and used the proxy whenever the value was valid.
requests trusts the proxy through verify, not the operating system. With the test CA installed through update-ca-certificates, curl, pip, httpx, Go, git and apt all accepted the TLS proxy and requests did not. requests passes its CA bundle (certifi by default, or REQUESTS_CA_BUNDLE, or verify=) to urllib3, which uses it for the proxy handshake as well. pip, which vendors requests, also hands its own SSL context to urllib3 as proxy_ssl_context (its source says so, citing pip issue 13465), and in the test it accepted the CA from the OS store.
Why https:// to a plain proxy says "wrong version number"#
The error message names a symptom four layers away from the cause. With https:// in the value, the client's first bytes on the TCP connection are a TLS ClientHello, which starts 16 03 01: record type 22 (handshake), version 3.1. Squid on a plain http_port reads that as a malformed HTTP request and answers it in HTTP:
HTTP/1.1 400 Bad Request
Server: squid/6.12
X-Squid-Error: ERR_PROTOCOL_UNKNOWN 0
Connection: closeThe client is waiting for a TLS ServerHello, so it parses the reply as a TLS record header. The first five bytes are 48 54 54 50 2f, the text HTTP/. 0x48 is not a valid record type and 0x5454 ("TT") is not a TLS version, so OpenSSL reports wrong version number, GnuTLS reports An unexpected TLS packet was received, and Go reports first record does not look like a TLS handshake. On the proxy side the only trace is one access log line with no method and no URL:
1792226613.707 0 10.201.10.4 NONE_NONE/400 3658 - error:invalid-request - HIER_NONE/- text/htmlA proxy administrator who sees a stream of NONE_NONE/400 error:invalid-request lines from one client address is looking at this mistake from the other end. Other proxies answer differently or not at all (tinyproxy, tried first, sent nothing back, and curl waited until the test's 20 second limit), so a hang on the first request through a new proxy setting is worth the same check.
What the scheme in HTTPS_PROXY controls, and what it does not#
There are up to two TLS sessions when a client fetches an HTTPS URL through a proxy, and the scheme in HTTPS_PROXY controls only the outer one.
http://proxy:3128. TCP to the proxy, a plain-textCONNECT example.com:443 HTTP/1.1, a200from the proxy, then the client's TLS handshake withexample.comthrough the tunnel. The proxy sees the target host and port and anyProxy-Authorizationheader; it does not see the URL path, the headers or the body. The mechanics are in HTTP CONNECT tunnelling.https://proxy:3129. A TLS session with the proxy first, then the sameCONNECTand the same end-to-end TLS session inside it: TLS inside TLS. What the extra layer adds is confidentiality for theCONNECTline and for the proxy credentials, and authentication of the proxy.
What neither form changes is whether the proxy can read your HTTPS traffic. That depends on whether it intercepts TLS by re-signing certificates with a corporate root, which is a separate decision covered in TLS interception and corporate root CAs. An intercepting proxy is usually reached with http:// all the same.
Why so many guides say https://#
The variable name reads like an instruction. HTTPS_PROXY sounds like "the HTTPS proxy". It is the proxy for HTTPS requests: Go's httpproxy package describes HTTPSProxy as the value "used as the proxy URL for HTTPS requests", and Node's documentation calls HTTPS_PROXY the "Proxy server URL for HTTPS requests". HTTP_PROXY is the proxy for plain-HTTP requests, and the two very often hold the same http:// URL.
Python ignored the scheme for years. Before urllib3 1.26.0, released on 10 November 2020, urllib3 could not open TLS to a proxy at all, and its 1.25.9 changelog added a warning for anyone "erroneously specifying an HTTPS proxy URL", saying that urllib3 "doesn't currently support connecting to HTTPS proxies". Version 1.26.0 "added support for HTTPS proxies contacting HTTPS servers", and pip 20.3 (30 November 2020) upgraded its vendored urllib3 to 1.26.2. The old behaviour is easy to reproduce: requests 2.25.1 with urllib3 1.25.11 on Python 3.9, given HTTPS_PROXY=https://plainproxy:8888, sent a plain-text CONNECT, Squid logged TCP_TUNNEL/200, and the request succeeded. A https:// value that had worked in pip for years therefore started failing when pip was upgraded. urllib3 1.26.8 added the "Your proxy appears to only use HTTP" hint to the error.
Two tools still ignore it. GNU Wget 1.25.0 accepts an https:// proxy only for HTTPS URLs (its retr.c rejects any other mismatch with Must be HTTP) and then speaks plain HTTP to it, which is why it passed against the plain proxy and failed against the TLS one. CPython's urllib.request behaved the same way. A value tested with either of them proves nothing about the rest of the toolchain.
curl's support is relatively recent. curl's documentation dates HTTPS proxy support to 7.52.0 for OpenSSL and GnuTLS builds, and to 7.87.0 for mbedTLS, Rustls, Schannel and wolfSSL. Long-lived install guides predate both.
When https:// is the right answer#
When the proxy listens with TLS. You can tell from how the proxy is advertised:
- A PAC file that returns
HTTPS proxy.corp:443describes a TLS proxy;PROXY proxy.corp:3128describes a plain one. Not every client understands theHTTPSkeyword, which the PAC files and WPAD page covers. - Squid configured with
https_port(which needs atls-cert=) accepts TLS from clients;http_portdoes not. Squid's own documentation describeshttps_portas "most useful" in accelerator mode, so a forward proxy onhttps_portis a deliberate choice somebody made. - A proxy reached on 443 is a hint, not proof.
Or test it from the client:
# A plain proxy: this succeeds, and Squid logs TCP_TUNNEL/200
curl -sS -o /dev/null -w '%{http_code}\n' -x http://proxy.corp:3128 https://example.com/
# A TLS proxy: this prints the proxy's own certificate chain
openssl s_client -connect proxy.corp:3128 -servername proxy.corp </dev/null 2>&1 | grep -E 'subject=|issuer=|Verify return'If s_client prints wrong version number and no peer certificate available, as it did against the test Squid with OpenSSL 3.5.9, the port is plain HTTP and the value must be http://. curl through a proxy has the verbose output to read when the first command fails for a different reason.
Trusting a TLS proxy's certificate, client by client#
A real TLS proxy adds a certificate that has nothing to do with the origin's, and every client keeps a separate setting for it, or none. The rows below are what made each client accept the test CA:
| Client | What worked for the proxy's certificate | What did not |
|---|---|---|
| curl | OS trust store, or --proxy-cacert ca.pem | --cacert alone: that is for the origin |
| git | OS trust store, http.proxySSLCAInfo, or GIT_PROXY_SSL_CAINFO | |
| requests | REQUESTS_CA_BUNDLE, or verify="bundle.pem" | OS trust store, SSL_CERT_FILE |
| pip | OS trust store, or --cert bundle.pem | |
| httpx | OS trust store, or SSL_CERT_FILE | |
| Node, npm | NODE_EXTRA_CA_CERTS | Nothing else tested |
| Go | OS trust store | |
| apt | OS trust store |
A bundle given to requests or pip replaces the default one rather than adding to it, so it must contain the public roots as well as the proxy CA: the test used certifi's bundle with the test CA appended. The wider question of which root CA each tool reads, for interception rather than for the proxy itself, is covered in corporate proxies and developer tooling.
The value without a scheme, and without a port#
HTTPS_PROXY=proxy.corp:3128 looks harmless and is not portable. curl, Go, git and httpx assume http://; Go's documentation states the rule ("a complete URL or a host[:port], in which case the http scheme is assumed"). requests and pip reject it with InvalidProxyURL, npm with EINVALIDPROXY, apt with Unsupported proxy configured: proxy.corp://3128 (it read the host as a scheme), and Node with NODE_USE_ENV_PROXY=1 exits before running any code.
Leaving out the port is just as uneven. curl documents a default proxy port of 1080 whatever the scheme, and curl -v with HTTPS_PROXY=http://plainproxy showed it trying port 1080, while requests, Node and Go tried 80. With https:// and no port, curl, requests, Node and Go all tried 443. Write the scheme and the port every time, and keep HTTPS_PROXY and https_proxy identical, because each client reads one or the other first (Node and Go document that the lower-case form wins); the same applies to the exclusion list in the no_proxy environment variable.
Where this answer stops applying#
The results are for the versions listed above, on Linux, against Squid and a socat TLS front end. Other proxy software answers a stray ClientHello differently, so the error text may differ while the cause is the same. Clients not in the table, Java, .NET, PowerShell, Docker's daemon and Windows' WinHTTP among them, were not tested here and have their own proxy settings. HTTPS_PROXY set to a socks5:// URL is a third case with its own rules, and so is a proxy that needs NTLM or Kerberos rather than Basic. Older builds behave differently in documented ways: curl before 7.52.0 and urllib3 before 1.26.0 cannot use a TLS proxy at all, and Node before 24.0.0 does not read these variables at all.
Frequently asked questions#
Should HTTPS_PROXY be http:// or https://?#
http://, for almost every proxy. The scheme describes the connection from the client to the proxy, and an ordinary forward proxy accepts plain HTTP and tunnels your TLS with CONNECT. Use https:// only if the proxy itself listens with TLS, which its operator will have told you or which a PAC entry starting HTTPS shows.
Is it insecure to use http:// for HTTPS_PROXY?#
Not for the content of your HTTPS requests. The TLS session with the origin runs end to end inside the CONNECT tunnel, so the proxy sees only the target host and port. What travels in clear is the CONNECT line and any Basic Proxy-Authorization header, which is the reason some operators run a TLS proxy.
How do I fix "Your proxy appears to only use HTTP and not HTTPS"?#
Change the scheme in HTTPS_PROXY (and https_proxy) from https:// to http://, keeping the host and port. requests and urllib3 print that hint when TLS to the proxy fails with WRONG_VERSION_NUMBER, which means the proxy answered in plain HTTP.
Why does pip say SSL WRONG_VERSION_NUMBER behind a proxy?#
Because HTTPS_PROXY starts with https:// while the proxy speaks plain HTTP. pip has opened TLS to https:// proxies since pip 20.3, which upgraded its vendored urllib3 to 1.26.2; older pip, like requests 2.25.1 with urllib3 1.25.11 in the test, sent plain HTTP to the proxy whatever the scheme said, which is why the same setting used to work.
Why does wget work with https_proxy=https:// when curl fails?#
GNU Wget 1.25.0 speaks plain HTTP to the proxy even when the value says https://, so it succeeds against a plain proxy by ignoring what you wrote. curl honours the scheme and opens TLS to the proxy, which a plain proxy cannot answer. BusyBox wget, the default on Alpine, does not read https_proxy at all.
Does Node.js use HTTPS_PROXY?#
Only when NODE_USE_ENV_PROXY=1 is set or --use-env-proxy is passed. The variable was added in Node 24.0.0, and support for the http and https modules in 24.5.0. Without it, fetch and https.get connect directly. npm reads the proxy variables itself and does not need the flag.
Can I leave the scheme off, as in HTTPS_PROXY=proxy:3128?#
Not portably. curl, Go, git and httpx assume http://, but requests and pip reject the value as an invalid proxy URL, npm and apt refuse it, and Node with the proxy flag exits at startup. Always write http://host:port.
Primary sources#
Every normative claim on this page is checked against the specification or the vendor documentation listed here. Where behaviour is version dependent, the version is named in the text.
- curl documentation, --proxy (schemes, default port, HTTPS proxy since 7.52.0)
- curl documentation, --proxy-cacert
- urllib3 changelog (1.25.9, 1.26.0, 1.26.8, 2.8.0)
- urllib3 advanced usage, HTTPS proxy error with an HTTP proxy
- pip changelog, 20.3 upgrades urllib3 to 1.26.2
- pip source, network/session.py (proxy_ssl_context for HTTPS proxies)
- Node.js CLI documentation, NODE_USE_ENV_PROXY
- Node.js http documentation, Built-in Proxy Support
- Go source, net/http ProxyFromEnvironment and x/net/http/httpproxy
- Git documentation, http.proxy and http.proxySSLCAInfo
- GNU Wget manual, proxies; and src/retr.c proxy scheme check
- apt-transport-http(1), Debian 13 (proxy URI schemes)
- Squid configuration directive https_port
Found something wrong, or behaviour that differs on your version? Report it with the version number and a primary source. Anything substantive is fixed in the page and logged on the corrections page. See editorial standards for how pages are researched, sourced and reviewed.
More in troubleshooting proxies#
- 502 vs 503 vs 504: reading proxy errors correctly
- Cntlm vs Px: which one for an NTLM or Kerberos proxy?
- Access-Control-Allow-Origin multiple values: the fix
- Header and body size limits at the proxy
- netsh winhttp vs Windows proxy settings: who reads which?
- nginx 499: what it means, and its ALB, HAProxy, Envoy names
- nginx 502 after Docker restart: fix the stale IP
- nginx connect() failed to [::1] with localhost: the fix
- nginx SSL handshaking to upstream errors: fix each one
- nginx cuts off large downloads: find which of 5 causes
- nginx worker_connections: the right number for a proxy
- A proxy debugging playbook