Guide

How to Fix 403 Forbidden Errors When Using a Proxy

A 403 through a proxy can come from the site or from the proxy. One command separates them, and the fix is different for each.

HProxy Team··Updated September 16, 2026·11 min read
HProxy.Guide

Skip the dead lists.

Our free proxy list re-checks every exit every few minutes across 100+ countries, with a live last-checked time, so you copy IPs that worked moments ago, not a stale text dump.

Open the free proxy list

You point curl at a site through your proxy, wait for the HTML, and get one line back: 403 Forbidden. No page. No redirect. No login form. The proxy connected, the server answered, and the answer was no.

That is the part that trips people up. Nothing actually broke. The TCP handshake happened, your proxy relayed the bytes, and the target read every header you sent. Then it looked at the request and decided you were not welcome. A 403 is a decision, not a failure, and that changes how you fix it.

What causes a 403 Forbidden error through a proxy?

A 403 Forbidden error through a proxy means a server received your request and refused it. The connection worked, so nothing is broken. Usually that server is the site, because the proxy IP is flagged, your request headers look automated, you hit a rate limit, or a web application firewall flagged the traffic. Sometimes it is the proxy itself, refusing on its own policy, and the next section separates the two in one command. HTTP's own wording is precise here: RFC 9110 section 15.5.4 defines 403 as the server having understood the request and refused to fulfill it. Understood is the operative word. Your bytes arrived and parsed cleanly, so the refusal is a decision layered on top of a connection that already worked.

First: who sent it?

One thing has to be settled before any fix, because the same status code comes from two different places. The site can refuse you, and the proxy can refuse you, on its own policy, before the site ever sees the request. RFC 9110 says the server understood the request and refused it, and a proxy is a server.

We built both cases on 16 September 2026, with a stub proxy and a stub site on our own machine, and asked curl for a plain page and an https page through each:

What we asked forWho refusedWhat came back
plain pagethe proxyexit 0, 403, with Via: 1.1 stub-proxy and Proxy-Agent headers and the proxy's own body
https pagethe proxyexit 56, CONNECT tunnel failed, response 403, the site never reached
plain pagethe siteexit 0, 403, with the site's Server header and no Via
https pagethe siteexit 0, HTTP/1.1 200 Connection established first, then the site's 403

That gives two checks, and both take one command:

# Ask for an https page. The answer is unambiguous.
curl -i --proxy http://203.0.113.7:8080 https://example.com/

# CONNECT tunnel failed, response 403  ->  the proxy refused. The site is not involved.
# 200 Connection established, then 403  ->  the proxy did its job and the site refused.
# Ask for a plain page and read the headers.
curl -i --proxy http://203.0.113.7:8080 http://example.com/

# A Via or Proxy-Agent header on the 403  ->  the proxy answered.
# The site's own Server header and no Via  ->  the site answered.

The Via header is what makes the second check work: it is added by proxies on the way through, so a refusal that carries one came from a machine in the middle. Not every proxy sets it, which is why the https test is the reliable one.

There is a third sender worth knowing. A protection layer in front of the site can refuse before the site does, and Cloudflare marks its own challenge responses with a cf-mitigated header, documented here.

And if a script is doing the asking, make it notice. In our run the proxy refusal left curl at exit code 0 with the proxy's page as the body, so a job that checks only the exit code recorded a success for a page it never fetched. Adding --fail turned the same request into exit 22 with the status in the message.

The rest of this page is about the case where the site refused you, which is the common one.

A 403 is a block, not a breakdown

It helps to separate the two failure modes you see behind a proxy.

If the proxy could not reach the target at all, you get a timeout or a gateway error. That is the proxy or an upstream hop giving up before an answer comes back, and we walked through that case in how to fix a 504 gateway timeout through a proxy.

A 403 is the opposite situation. The answer came back fast and clear. The server is not overloaded and the route is fine. It simply refused to serve you. So none of the usual timeout fixes apply. Bumping your client timeout or switching to a faster proxy will not move a 403 one inch, because speed was never the problem.

The five reasons a target hands you a 403

Almost every 403 behind a proxy comes down to one of these.

The IP is flagged or obviously datacenter. This is the big one. Anti-bot vendors keep running lists of IP ranges that belong to hosting providers and known proxy pools, the reputation half of how websites detect proxies. If your exit IP sits in one of those ranges, plenty of sites return a 403 on the first request, before they even look at what you sent. Free and heavily shared IPs are the worst offenders, which is why a pool pulled from a public free proxy list can get blocked on sites that a clean residential IP walks right into.

Your headers look like a script. A real browser sends a dozen headers on every request. A bare curl or a default Python client sends almost none. When a site sees no User-Agent, or User-Agent: python-requests/2.31, or a request with no Accept-Language at all, that is a cheap and reliable bot signal. Some firewalls return a 403 on that alone.

You tripped a rate limit. Fire fifty requests a second from one IP and the site starts refusing you, often with a 403 rather than a 429. This is common when you are scraping and forget to pace yourself. If that is your use case, our notes on proxies for web scraping go deeper on request pacing and rotation.

The content is geo blocked. Some 403s are geographic. The site serves visitors from one region and refuses the rest. If your proxy exits in a country the target does not allow, you get a 403 no matter how clean the IP or how perfect the headers. The fix is an exit in an allowed region, not more retries.

A WAF threw a challenge. Cloudflare, Akamai, and similar services sit in front of the origin and score every request. When the score looks automated they can answer with a 403 instead of a JavaScript challenge page, the "Access denied" block page you have seen on Cloudflare-protected sites, which scraping past Cloudflare takes apart in full. A telltale sign is a Server: cloudflare header sitting next to your 403, sometimes with a cf-mitigated: challenge line under it. Cloudflare documents that its challenge responses carry that exact header with challenge as the only value it takes, and serve a text/html body no matter what resource you asked for, so a cf-mitigated line is a reliable signal you hit the edge and not the origin.

How to fix a 403 through a proxy

Work these in order. The first two are free and clear most cases.

Fixing a 403: the connection worked, so fix the request
  1. 403 Forbidden

    site refused you

  2. Real browser headers

    UA, Accept-Language

  3. Clean residential IP

    if the IP is flagged

  4. 200 OK

    a normal visitor again

Source: Headers and IP reputation clear most 403s

1. Confirm the proxy is elite, not transparent

A transparent proxy leaks your real IP to the target in headers like X-Forwarded-For or Via. X-Forwarded-For is the de-facto standard MDN documents for carrying a client's originating IP through a proxy chain, and Via is defined in RFC 9110 section 7.6.3 to record the intermediaries a message passed through. Either one on your request announces to the target that it is talking to a proxy. The site sees both that you are proxying and where you really are, and some firewalls 403 on that immediately. An elite (high anonymity) proxy sends none of those. Run your proxy through our proxy checker, which echoes back the exact headers the destination receives. If your real IP or a Via header shows up, the proxy is not elite, and you want a better one before you touch anything else.

2. Send headers that look like a real browser

This one is quick and it clears a surprising number of 403s. Here is a request that gets refused, sent as a bare proxy call:

curl -sSI -x http://203.0.113.7:8080 https://example.com/login
HTTP/1.1 403 Forbidden
Date: Fri, 03 Jul 2026 09:14:22 GMT
Content-Type: text/html; charset=UTF-8
Server: nginx

This is a plain header-check block, the kind a basic bot filter throws when it sees a bare python-requests or empty User-Agent. It clears with real headers. A Cloudflare managed challenge (the cf-mitigated: challenge case above) is a different animal: it weighs IP reputation and TLS fingerprint far above headers, so on a datacenter IP no header set alone fixes it, and you go to step 3.

Now the same request with a real browser fingerprint in the headers:

curl -s -o /dev/null -w '%{http_code}\n' \
  -x http://203.0.113.7:8080 \
  -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36' \
  -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8' \
  -H 'Accept-Language: en-US,en;q=0.9' \
  -H 'Accept-Encoding: gzip, deflate, br' \
  https://example.com/login
# 200

Send a current User-Agent, an Accept-Language, a full Accept, and Accept-Encoding. Match a browser you actually run. Do not invent a UA string that no real Chrome ever sent, because a few firewalls check the version against the TLS fingerprint, and a mismatch is its own flag.

3. Move to clean residential IPs

If the IP itself is the problem, no header trick saves it. You need an exit that anti-bot systems trust, and that means residential proxies on real consumer connections rather than datacenter ranges. Residential IPs carry the reputation of an ordinary home visitor, so the same request that got a 403 from a hosting IP often returns a 200 from a residential one.

4. Slow down and spread the load

If the block only shows up after a burst, you are rate limited. Add a short delay between requests, cap your concurrency, and rotate across a pool so no single IP carries the whole load. A steady, human pace beats a fast one that gets you refused after request forty.

5. Retry the exact request on a fresh IP

When a specific IP is burned, the cleanest move is to send the same request again through a different exit. If the flagged datacenter IP above keeps failing, retry on a clean residential exit:

curl -s -o /dev/null -w '%{http_code}\n' \
  -x http://198.51.100.14:8080 \
  -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36' \
  -H 'Accept-Language: en-US,en;q=0.9' \
  https://example.com/login
# 200

Build this into your client: on a 403, switch IP and retry once or twice before you treat the request as truly failed. Do not hammer the same burned IP in a loop, because that just deepens the block.

A quick order of operations

When a 403 lands, run down this list instead of guessing:

  1. Is it really a 403 and not a timeout? A timeout is a different problem with different fixes.
  2. Does the proxy leak your IP? Check anonymity first.
  3. Are you sending real browser headers? Add them if not.
  4. Is the IP datacenter or shared? Move to a clean residential exit.
  5. Are you going too fast? Slow down and rotate.
  6. Is the content geo locked? Pick an allowed region.

Nine times out of ten the answer is a flagged IP, weak headers, or both, and you clear the block long before you reach the bottom of that list.

When the 403 is not about the proxy at all

One more thing, because chasing the proxy when the proxy is fine wastes hours. Some 403s are the site telling you that you are not authenticated. If the endpoint needs a login cookie, a session token, or an API key and you are not sending it, you get a 403 from any IP on earth, proxy or not. Test the same request from your own connection with the right credentials first. If it still 403s at home, the block is about auth or permissions, and no proxy change will fix it. If it works at home but fails through the proxy, then you are back to the five causes above.

Get the IP reputation and the headers right and the 403 usually goes away, because at that point you are just a normal visitor asking for a normal page.

If you would rather skip flagged IPs entirely, our residential proxies carry the clean reputation of an ordinary home visitor, and the proxy checker tells you whether an exit is elite before you ever route a request through it.

Sources

  • RFC 9110 section 15.5.4, June 2022: "The 403 (Forbidden) status code indicates that the server understood the request but refuses to fulfill it."
  • RFC 9110 section 7.6.3 and MDN on Via, read 16 September 2026: the header "is added by proxies, both forward and reverse", for tracking message forwards and avoiding loops.
  • RFC 9110 section 9.3.6, June 2022, for the CONNECT request that an https page makes through a proxy.
  • Cloudflare, detecting a challenge response, read 16 September 2026, for the cf-mitigated header on a challenge.
  • libcurl error codes, read 16 September 2026, for exit 56 and exit 22.
  • Measured by HProxy on 16 September 2026 with curl 8.16.0: a stub proxy that refuses, a stub proxy that relays and a stub site that refuses, all on this machine, asked for a plain page and an https page. Nothing outside the machine was contacted.

Frequently asked questions

Does a 403 error mean my proxy is broken?
Usually not, and not always. Most of the time the connection succeeded and the site refused your request, which is a decision rather than a fault. But a proxy can refuse on its own policy too. We measured both on 16 September 2026: a proxy refusal of an https page gives curl exit 56 with CONNECT tunnel failed, response 403, and the site is never reached, while a site refusal shows the tunnel being established first. On a plain page, a Via or Proxy-Agent header on the response is the proxy's fingerprint.
Why do I get a 403 with a proxy but not from my home connection?
Your home IP is a residential address with a clean reputation, so sites trust it. Many proxy IPs, especially datacenter ranges, already sit on anti-bot blocklists and get refused on sight. Switching to residential proxies with clean IPs usually clears a 403 that only happens through the proxy.
Will rotating IPs fix a 403 error?
Often yes, when the block is tied to one flagged IP or a rate limit. Retrying the same request on a fresh, clean IP gets you past IP-level bans. Rotation will not help if the real cause is missing browser headers or a WAF challenge, so fix those at the same time.
What request headers help avoid a 403?
Send a current browser User-Agent, an Accept-Language header such as en-US, a full Accept header, and Accept-Encoding. Requests with no User-Agent or a default library one like python-requests are the easiest bot signal for a site to block. Match what a real Chrome or Firefox request sends.
How do I know if my proxy is elite or transparent?
A transparent proxy leaks your real IP in headers like X-Forwarded-For or Via, so the target can tell you are proxying. An elite proxy sends none of those. Run the proxy through a checker that echoes the headers the destination receives, and if your real IP shows up, the proxy is not elite.
Why do real browser headers fix some 403s but not others?
Because two different blocks share the same status code. A basic header filter refuses anything that does not look like a browser, no User-Agent or a python-requests default, and real headers clear it outright. A reputation or TLS-fingerprint block, like a Cloudflare challenge marked with a cf-mitigated header, weighs the exit IP and the connection far above headers, so on a flagged datacenter IP no header set alone gets through and you need a clean residential exit.

Get proxies that are alive right now

Our free proxy list re-checks every exit every few minutes across 100+ countries, with a live last-checked time, so you copy IPs that worked moments ago, not a stale text dump. When the location has to survive a real check, the paid network holds up.

129M+ proxy checks run · 100+ countries · HTTP / HTTPS / SOCKS · re-checked every few minutes · no signup

HProxy.

Honest guides and comparisons on proxies, scraping and staying unblocked, from the team that runs the network.

RSS feed