Proxy Not Working on Certain Websites? How to Fix It
Proxy not working on certain websites but fine everywhere else? Tell a block from a port, TLS/SNI or DNS fault with one command each, then apply the right fix.
A proxy not working on certain websites while every other site loads is almost never a dead proxy. If the gateway carries your traffic to ten sites and fails on the eleventh, the connection to the proxy, your credentials and your client config have already been proven by the ten. Something specific to that one target is saying no. This guide gives you one command per suspect (a block, a port restriction, a TLS/SNI fault, a DNS mismatch), shows the exact error text each one produces, and tells you plainly which of them a proxy can fix.
The Error Text, Hop by Hop
Before changing anything, capture the literal error. The wording tells you which hop failed, and different hops have different owners. We reproduced each failure below on 2026-10-30 against a local test proxy, using curl 8.15.0 (Windows, Schannel build), OpenSSL 3.2.4, and Python Requests 2.32.5 with urllib3 2.6.3. Strings from OpenSSL-linked curl builds on Linux word the TLS case differently, but the exit codes match.
| What you see | Tool | Hop that failed | Likely owner |
|---|---|---|---|
| `curl: (56) CONNECT tunnel failed, response 403` | curl | Proxy refused the tunnel | Proxy policy (often the port) |
| `curl: (56) CONNECT tunnel failed, response 502` | curl | Proxy could not reach or resolve the host | DNS or upstream path |
| `OSError('Tunnel connection failed: 403 Forbidden')` | Requests | Same as the curl 403 above | Proxy policy |
| `s_client: HTTP CONNECT failed, reason=403 Forbidden` | openssl | Same, seen by OpenSSL | Proxy policy |
| `curl: (35) schannel: failed to receive handshake, SSL/TLS connection failed` | curl | TLS inside the tunnel | Target edge or a middlebox |
| `SSLEOFError(8, '[SSL: UNEXPECTED_EOF_WHILE_READING] EOF occurred in violation of protocol (_ssl.c:1081)')` | Requests | TLS inside the tunnel | Target edge or a middlebox |
| `curl: (97) Could not resolve proxy: 127.0.0.1` with a `socks5://` URL | curl | Your local DNS lookup of the target | Your machine's resolver |
| `curl: (97) cannot complete SOCKS5 connection to | curl | SOCKS server could not reach the host | DNS or upstream path |
| HTTP `403`, `429`, `503` with a vendor page | any | None. The site answered | The target |
Look at the 97 row twice. In our test the SOCKS proxy at 127.0.0.1 was up and reachable. With socks5://, curl 8.15.0 tried to resolve the target locally, failed, and printed a message naming the proxy. People read that line and start debugging a proxy that is fine.
Why a Proxy Not Working on Certain Websites Means Rejection
An HTTPS request through an HTTP proxy passes four gates in order: TCP to the proxy, a CONNECT host:port request (with your auth), a TLS handshake with the target carried inside the tunnel, and finally the HTTP request and response. A failure is only informative once you know which gate it happened at.
When other sites work through the same proxy, gates one and two are proven for your account. Bad credentials would fail everywhere with a 407. A wrong port on the gateway would fail everywhere with a connect error. So the remaining suspects are all things that differ per destination: the port in the URL, the hostname the proxy has to resolve, the TLS edge that sits in front of the site, and the site's own decision about your request.
That last one is by far the most common. Sites that run bot management judge the exit IP, its ASN, its country, your request rate and your client fingerprint. A news site with no protection serves anyone; a ticketing or retail site behind an anti-bot vendor refuses the same request. That isn't a proxy outage. It's a target with a different policy.
If you haven't confirmed the proxy works at all, do that first with the basic checks in how to test if your proxy is working. Everything below assumes it passes for other sites.
Scraping at scale? Skip the blocks.
Fast, unblockable datacentre proxies with unlimited bandwidth.
Four Commands, Run in This Order
Run these in order. Each takes seconds and either clears a suspect or names it.
| Order | Suspect | Command (one each) | Clears it when |
|---|---|---|---|
| 1 | Block by the site | `curl -w "%{http_connect} %{http_code}"` | You get a CONNECT 200 and a normal page |
| 2 | Port restriction | `curl -v` and read the CONNECT reply | The proxy answers the CONNECT with 200 |
| 3 | TLS or SNI fault | `openssl s_client -proxy ... -servername` | The handshake completes and verification is OK |
| 4 | DNS mismatch | swap `socks5://` for `socks5h://` | Results are identical with both schemes |
The order isn't arbitrary. The block test reads two numbers that already split the problem in half: if the proxy's CONNECT reply is 200 and the site's status is a 403, you're done diagnosing and can skip to the fix.
Check 1: Did the Site Answer? (Block Test)
curl's --write-out exposes both the proxy's answer to CONNECT and the target's HTTP status in one line.
curl -sS -o page.html \
-x "http://USER:PASS@gateway.sparkproxy.io:11000" \
-w "connect=%{http_connect} status=%{http_code} ip=%{remote_ip}\n" \
https://shop.sparkproxy.io/product/123
Read the result like this:
connect=200 status=200: the request worked. If your scraper still "fails", the problem is in parsing or in a JavaScript-rendered page your HTTP client never executes.connect=200 status=403(or 429, 503): the proxy did its job and the site refused you. This is a block.connect=403orconnect=502withstatus=000: the proxy itself refused or failed the tunnel, so the site never saw you. Go to check 2 or 4.- A curl error with no numbers: the tunnel opened and something broke afterwards, usually TLS. Go to check 3.
Once it's a block, open page.html. The vendor's page, cookie names and headers identify who blocked you, and the full method for reading them is in how to identify which anti-bot a site uses. You need that name before choosing a fix, because an IP-reputation block and a JavaScript challenge need different things.
Check 2: Did the Proxy Refuse the Port?
Most proxies only open tunnels to a short list of destination ports. Squid's documented default rules include http_access deny CONNECT !SSL_ports, under the comment "Deny CONNECT to other than secure SSL ports" (squid-cache.org, http_access directive, read 2026-10-30). Commercial gateways apply their own lists for the same reason: an open CONNECT to port 25 is a spam relay.
So if the one failing site lives on :8443, :2083, :9443 or any non-standard port, look at the CONNECT exchange:
curl -v -o /dev/null \
-x "http://USER:PASS@gateway.sparkproxy.io:11000" \
https://panel.sparkproxy.io:8443/ 2>&1 | grep -E "^[<>] |CONNECT"
In our reproduction the refusal looked like this:
> CONNECT panel.sparkproxy.io:8443 HTTP/1.1
> Host: panel.sparkproxy.io:8443
< HTTP/1.1 403 Forbidden
* CONNECT tunnel failed, response 403
The giveaway is that the same host on port 443 works. A 403 on the CONNECT line comes from the proxy, never from the website, because the website hasn't been contacted yet. Changing IPs won't help, rotation won't help, and nothing in your code will help. Ask the provider which ports are permitted (we don't publish SparkProxy's port allowlist here because we couldn't verify it on a public page today). For how ports map to protocols in general, see proxy ports explained.
Check 3: Did the TLS Handshake Die? (SNI Test)
If CONNECT returns 200 and the connection then dies, the failure is in the TLS handshake inside the tunnel. OpenSSL can open the tunnel and run the handshake for you, so you see exactly where it stops:
openssl s_client -brief \
-proxy gateway.sparkproxy.io:11000 \
-proxy_user USER -proxy_pass pass:PASS \
-connect shop.sparkproxy.io:443 \
-servername shop.sparkproxy.io </dev/null
The -proxy_user and -proxy_pass flags are documented in the OpenSSL 3.0 s_client manual and present in our 3.2.4 build; older builds may lack them, in which case use IP whitelisting for this test. A healthy result prints CONNECTION ESTABLISHED, the protocol (we got TLSv1.3), the peer certificate and Verification: OK.
Three failure shapes matter:
- EOF right after the ClientHello. We got
error:0A000126:SSL routines::unexpected eof while readingwhen our test proxy dropped the tunnel after the first TLS bytes. Something between you and the origin read the SNI and hung up. That can be the site's edge refusing that exit range at the TLS layer, or a filter on your own network that inspects SNI. Run the same command without-proxy: if it succeeds direct and fails through the proxy, the edge dislikes the exit; if it fails both ways, it's your network. - A handshake alert. Dropping SNI entirely with
-noservernameagainst a Cloudflare-fronted site gave usssl/tls alert handshake failure ... SSL alert number 40. Edges that host thousands of names need SNI to pick a certificate. If your client is sending an IP address, an empty name, or a name the edge doesn't host, you'll see this. The cure is in your code: connect by hostname, not by a hard-coded IP. - Verification failure (
unable to get local issuer certificate, curl exit 60). Someone presented a certificate your trust store doesn't accept. Through a plain CONNECT proxy, the certificate comes from the target, so this usually points to inspection by antivirus or a corporate gateway on your side, not the proxy. Fix the trust chain; never shipverify=False.
A fourth TLS cause doesn't show up as an error at all: TLS fingerprinting. The handshake completes and the site then serves a 403 or a challenge because your client's ClientHello doesn't look like a browser. That lands in check 1 as a block, and no proxy changes it, since the fingerprint is produced by your client library. Background is in what is TLS fingerprinting.
Check 4: Who Resolved the Hostname? (DNS Test)
With an HTTP proxy, your client puts the hostname in the CONNECT line and the proxy resolves it. With SOCKS5 it depends on the URL scheme. Requests' documentation says socks5 "causes the DNS resolution to happen on the client, rather than on the proxy server" and that socks5h resolves on the proxy; curl uses the same convention.
We confirmed it on the wire with a logging SOCKS server: socks5:// delivered ATYP 1 104.21.76.82 (an IPv4 address your machine looked up), while socks5h:// delivered ATYP 3 www.sparkproxy.io (the name). The one command for this check is a scheme swap:
curl -sS -o /dev/null -w "%{http_code}\n" -x "socks5://USER:PASS@gateway.sparkproxy.io:13000" https://shop.sparkproxy.io/
curl -sS -o /dev/null -w "%{http_code}\n" -x "socks5h://USER:PASS@gateway.sparkproxy.io:13000" https://shop.sparkproxy.io/
If socks5h works and socks5 fails, your local resolver is the problem. Three things cause it for a single site:
- Local filtering. A corporate DNS, Pi-hole or hosts-file entry blocks or redirects that one domain. In Python the failure surfaces before the proxy is involved; on Windows we saw
[Errno 11001] getaddrinfo failedinside aNewConnectionError. - Geo-steered answers. CDNs often answer DNS with an edge near the resolver. With local resolution you send the proxy an IP chosen for your location, then exit from another country. Some sites tolerate that, some don't, and some hand back the wrong regional content.
- Leaks. Local lookups for a target also reveal it to your ISP's resolver, which is the reason many people chose a proxy in the first place.
The fix is one character: use socks5h://. With the HTTP port, a resolution failure on the proxy side shows up as the CONNECT tunnel failed, response 502 line from the table, and in that case check the hostname for typos and whether the domain resolves publicly at all. More background: how proxies handle DNS resolution.
Who Owns Each Cause: You, the Provider, or the Site
| Cause | How it shows up | Can a proxy fix it? | What does fix it |
|---|---|---|---|
| Exit IP or ASN reputation | 403 or challenge, CONNECT 200 | Yes | A different exit, or a pool the site trusts |
| Geo restriction | 403, 451, or a redirect to a regional page | Yes | An exit in the right country |
| Per-IP rate limit | 429, often after N requests | Yes, partly | Rotation plus slower pacing |
| Port not allowed | CONNECT 403 | No (provider policy) | Ask the provider; use a permitted port |
| Local DNS filter or geo mismatch | getaddrinfo error, curl 97 | Yes, by config | `socks5h://` or the HTTP port |
| Missing or wrong SNI | TLS alert 40 | No | Connect by hostname in your client |
| TLS interception on your network | Exit 60, unknown issuer | No | Fix trust chain or bypass the inspector |
| TLS or HTTP/2 fingerprint | 403 after a clean handshake | No | A browser-like client, or a rendering API |
| JavaScript challenge | 200 or 403 with a challenge page | No | Real browser rendering |
| Site down or geo-fenced to a network you can't reach | Timeouts from every exit | No | Nothing on your side |
The honest summary: a proxy changes where you come from. It doesn't change what your client looks like, which ports your provider permits, or whether your own network tampers with TLS.
Fixing a Real Block: Rotation, Sessions, Rendering
If check 1 showed CONNECT 200 plus a refusal, start with the cheapest lever: a different exit. SparkProxy's gateway offers Random Rotation and Sticky Sessions (a 5-minute session). Use Random Rotation for independent page fetches. Use Sticky Sessions when the site ties state to an IP, such as a login, a cart or a paginated search; rotating mid-flow is a classic reason a site "works for one page then fails".
A pitfall in Requests: its docs warn that session.proxies can be overwritten by environment proxy variables, so pass proxies= on each call. Open a fresh connection per retry, otherwise keep-alive reuses the tunnel and the exit IP:
import os
import time
import requests
GATEWAY = "http://{}:{}@gateway.sparkproxy.io:11000".format(
os.environ["SP_USER"], os.environ["SP_PASS"]
)
PROXIES = {"http": GATEWAY, "https": GATEWAY}
BLOCK_CODES = {403, 429, 503}
def get_with_new_exit(url, attempts=4):
last = None
for attempt in range(attempts):
# A new Session means a new CONNECT, so Random Rotation can hand out a new exit.
with requests.Session() as s:
try:
r = s.get(url, proxies=PROXIES, timeout=(10, 30))
except requests.exceptions.ProxyError:
raise # CONNECT refused: a port or DNS problem, retrying burns time
except requests.exceptions.SSLError:
raise # TLS fault: see check 3, a new IP rarely helps
if r.status_code not in BLOCK_CODES:
return r
last = r
time.sleep(2 ** attempt)
return last
resp = get_with_new_exit("https://shop.sparkproxy.io/product/123")
print(resp.status_code, len(resp.text))
If four fresh exits all get refused, the block is probably not about one IP. It's about the exit type, the country, or your fingerprint, and the anti-bot identification step tells you which. At that point a rendering API is usually cheaper than building a browser stack. The SparkProxy Scraping API (base https://scrape.sparkproxy.io/api/v1, header X-API-Key, 1,000 free credits) fetches the page for you. Per the Scraping API docs read 2026-10-30: render_js defaults to true (5 credits), render_js=false is a plain fetch at 1 credit, country_code adds 5, and proxy_type=premium is listed at 10 credits without JS or 25 with it.
Escalate only when the cheap call fails. json_response=true returns the envelope (status_code, credits_used, body), and body is base64, so decode it before looking for block markers:
import base64
import os
import requests
API = "https://scrape.sparkproxy.io/api/v1"
HEADERS = {"X-API-Key": os.environ["SPARKPROXY_API_KEY"]}
# Replace with strings from the target's own block page.
MARKERS = ("access denied", "captcha", "verify you are human")
def scrape(url, **extra):
params = {"url": url, "render_js": "false", "json_response": "true"}
params.update(extra)
r = requests.get(API, params=params, headers=HEADERS, timeout=120)
r.raise_for_status()
env = r.json()
html = base64.b64decode(env.get("body") or "").decode("utf-8", "replace")
return env.get("status_code"), env.get("credits_used"), html
def blocked(status, html):
if status in (401, 403, 429, 503):
return True
low = html.lower()
return any(m in low for m in MARKERS)
target = "https://shop.sparkproxy.io/product/123"
status, cost, html = scrape(target)
if blocked(status, html):
status, cost, html = scrape(
target, render_js="true", proxy_type="premium", country_code="us"
)
print(status, cost, len(html), "blocked" if blocked(status, html) else "ok")
Don't use format=json for this. format only converts page content (html, md, mdx, json, screenshot, pdf); it's not the envelope, and code that reads status_code from it gets nothing. When we tried a live call on 2026-10-30 the API edge returned Cloudflare error code: 521, so the envelope fields above come from the docs, not from a response we captured.
On the gateway side, plans start from $75/month (USD, sparkproxy.io, read 2026-10-30) with unlimited bandwidth, so retries through the proxy cost concurrency rather than traffic. API escalations cost credits, which is why the code tries the 1-credit fetch first.
A Python Classifier That Labels the Failure
When a job touches hundreds of domains, you want each failure labelled automatically rather than read by hand. This maps the exception types and strings we measured to the four suspects. Order matters: ProxyError and SSLError are both subclasses of ConnectionError in Requests, so they must be caught first.
import sys
import requests
from requests.exceptions import (
ConnectionError,
ConnectTimeout,
ProxyError,
ReadTimeout,
SSLError,
)
def classify(url, proxy):
proxies = {"http": proxy, "https": proxy}
try:
r = requests.get(url, proxies=proxies, timeout=(10, 30), allow_redirects=False)
except ProxyError as e:
msg = str(e)
if "Tunnel connection failed: 407" in msg:
return "AUTH: proxy rejected credentials (would fail on every site)"
if "Tunnel connection failed: 403" in msg:
return "PORT/POLICY: proxy refused the CONNECT, check the port"
if "Tunnel connection failed: 50" in msg:
return "DNS/UPSTREAM: proxy could not resolve or reach the host"
return "PROXY: " + msg[:120]
except SSLError as e:
return "TLS: handshake failed inside the tunnel (" + type(e).__name__ + ")"
except ConnectTimeout:
return "TIMEOUT: connecting took too long"
except ReadTimeout:
return "SLOW: tunnel open, site never answered (possible tarpit)"
except ConnectionError as e:
if "getaddrinfo" in str(e) or "NameResolution" in str(e):
return "DNS/LOCAL: your resolver failed, use socks5h:// or the HTTP port"
return "CONNECTION: " + str(e)[:120]
if r.status_code in (401, 403, 429, 451, 503):
return "BLOCK: site answered {}".format(r.status_code)
return "OK: {}".format(r.status_code)
if __name__ == "__main__":
proxy_url = sys.argv[1]
for target in sys.argv[2:]:
print(target, "->", classify(target, proxy_url))
Run it with the proxy URL first and any number of target URLs after it. Mixed results across domains are normal; a single label repeated across every domain points at your setup instead.
When the Problem Belongs to Your Provider
Two outcomes from the checks above are the provider's to solve: a CONNECT 403 on a port you need, and a CONNECT 502 for a hostname that resolves fine publicly. A third is shared: if fresh exits from the whole pool are refused by one site while a different network reaches it with the identical client, that's evidence about the pool's reputation on that target.
Send the raw outputs, not a summary. The curl -w line, the -v CONNECT exchange and the s_client result answer most of a support engineer's first questions. How to package that into a ticket that gets actioned quickly is covered in how to escalate proxy blocks with your provider, and the meaning of each status code a gateway can return is in proxy error codes explained.
If you want to run all four checks against your own target with a clean pool, SparkProxy's 24-hour free trial needs no credit card.
Frequently asked questions
FAQ
Because the parts every site shares, the connection to the proxy and your credentials, are already proven by the sites that load. The failing site differs in its port, its hostname resolution, its TLS edge, or its decision to block your request, and one command per suspect tells you which.
The proxy refused to open a tunnel to that host and port, so the website was never contacted. It most often means the destination port isn't on the proxy's allowlist, so compare the same host on port 443 before blaming the site.
Run curl with -w "%{http_connect} %{http_code}". A CONNECT value of 200 with a site status of 403, 429 or 503 means the proxy delivered your request and the website refused it, which is a block rather than a proxy fault.
Use socks5h. With plain socks5 your machine resolves the hostname, so local DNS filters, geo-steered CDN answers and DNS leaks all apply, while socks5h lets the proxy resolve the name from its own location.
Sometimes. If the handshake works direct but fails through one exit, a different exit can help; but missing SNI, TLS interception on your own network, and TLS fingerprint blocks come from your side, and no proxy changes them.
When several fresh exits in a row are refused by the same site, the block is about fingerprint, exit type or a JavaScript challenge rather than one IP. A rendering API with country targeting handles those, and it's worth paying the higher credit cost only for that target.
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
Sticky Session IP Changing Mid-Flow? Causes and Fixes
Sticky session IP changing mid-login or checkout? Check leaks, session ID format, the 5-minute window, parallel connections and exit churn in order.
httpx proxies Argument Removed? Fix ProxyError and 407
The httpx proxies argument is gone in 0.28. Fix the TypeError, then ProxyError 407, SOCKS ImportError and RemoteProtocolError, with verbatim errors and code.
npm config set proxy: Install Packages Behind a Proxy
npm config set proxy and https-proxy, the right .npmrc file, yarn and pnpm equivalents, noproxy for private registries, and keeping passwords out of your repo.
