A 403 and a 407 are different failures. Read the headers first.

Which layer refused your proxied request, told from four details in the response: the status line, the challenge, the signature and the body.

  • Published November 6, 2026
  • 10 min read
In this article
  1. Two header blocks, and the first one is the hop's
  2. A 407 carries a challenge; a 403 carries a refusal
  3. The signature, not the status number, says who sent it
  4. The body and Retry-After decide whether to try again
  5. One script reads all four and names the layer
A request line passing through three thin gates set one behind the other, the middle gate alone lit, with a notched square plate on it and the line dashed beyond.

The ticket opens with a 407, and somebody fixes the credential. The next message says every request now comes back 403 and asks whether we've blocked the IP, and by then they've regenerated the list twice.

That second error is mostly good news. The 407 was our gateway's and the 403 was almost certainly the site's, so the credential got fixed somewhere between the two messages. Regenerating after that changes nothing: the site has never seen your dashboard.

Before I touch a credential or a header I read four things in the failed response. Ten seconds with curl. They say whether the proxy refused you, or the site's CDN edge, or the site. The script at the bottom reads all four.

Two header blocks, and the first one is the hop's

Fetch an https:// URL through an HTTP proxy and your client never hands the proxy the request. It sends CONNECT example.com:443; the proxy answers with its own status line and headers; only after a 200 does TLS to the target start. The target's response then comes back inside the tunnel with a second set of headers.

You can see both with -D -. This is a real 403 from a Cloudflare-fronted site, one request on 2026-11-06, sent through a throwaway CONNECT proxy I ran locally for the test (it isn't our gateway, because I didn't have a credential on that machine):

HTTP/1.1 200 Connection established
X-Test-Proxy: mini

HTTP/2 403
date: Fri, 06 Nov 2026 09:47:02 GMT
content-type: text/html; charset=UTF-8
content-length: 27613
cf-mitigated: challenge
server: cloudflare
cf-ray: a5632b19e9bbc34b-EWR

The first block is the proxy saying yes and opening the tunnel. The second is the site refusing the request, so the proxy did everything it was asked to do here.

Our gateway is stricter than that. For an https:// target it can only answer with 200 when the tunnel opens, or with 407, 502 or 503. If you see a 403 or a 429 in your logs, it came from the target and was carried back through the tunnel.

Plain http:// targets get no CONNECT. The proxy forwards the request and passes statuses through in one block, so block-counting tells you nothing there.

A 407 carries a challenge; a 403 carries a refusal

RFC 9110 §15.5.8 describes 407 as the proxy's version of 401, and it says the proxy MUST send a Proxy-Authenticate header with a challenge in it. A 401 comes from the origin and uses WWW-Authenticate, so the header tells you which hop is asking.

When I asked our gateway for a tunnel with no credentials on 2026-11-06, this is what came back, all of it:

curl -sS -o /dev/null -D - --max-time 20 \
  -x http://premium.resifactory.net:7777 https://example.com/
HTTP/1.1 407 Proxy Authentication Required
Proxy-Authenticate: Basic realm="Proxy"
X-ResiFactory-Request-Id: <issued per request>
Content-Length: 0
Connection: keep-alive

There's one block, one challenge and an empty body, and the target never got contacted.

A 403 means something else. Section 15.5.4 of the same RFC says the server understood the request and won't fulfil it, and §11.6 says a server that gets valid credentials which aren't good enough should send 403 rather than another challenge. A 403 in the second block comes after the proxy's 200, so your proxy login worked, even though the request didn't.

A Proxy-Authenticate header on its own isn't proof of a bad password either. Some libraries only send credentials after they've been challenged, so they take one 407 on purpose and answer it without telling you. The 407 to worry about is the one that's still there after the credential has gone out.

Ours has six documented causes for a 407 that sticks. Wrong username or password is one. A list made before a proxy-password reset is another: every Primary-password line stops working about 30 seconds after the reset, and the username looks unchanged. Then a revoked credential, an empty wallet for that pool, and demo lines, which start DEMO_USER, have the password INVALID, come 25 at most when you generate with no balance, and never authenticate. Sixth is a username hand-edited into something the gateway can't parse.

That last one catches people. A line is host:port:user:pass, and the user part is a set of positional fields joined by ~, starting with username, country, session id and minutes. Delete one field and everything after it moves over by one, which is enough to get you a 407 on a line that looks nearly right.

The signature, not the status number, says who sent it

The status number doesn't tell you who sent it, because anything inside a tunnel can send any status, a 407 included. So I look at who signed the response.

Ours carries X-ResiFactory-Request-Id next to Proxy-Authenticate: Basic realm="Proxy". A 407 with both of those came from our gateway, and the request id is the thing to paste into a ticket with us.

Cloudflare signs with cf-ray, usually next to server: cloudflare. Its HTTP headers reference (read 2026-11-06) says the Ray ID you receive is the value it sends to the origin too, so you and the site owner can both look it up. The 1020 page tells owners to search Security Events by Ray ID to find which rule fired. Think a refusal is wrong? Send the owner that cf-ray. A new exit IP answers a different question.

The capture above also had cf-mitigated: challenge in it. (Cloudflare's headers reference doesn't list that one as of 2026-11-06, so all I can say is that it was in this response.) And if a 403 has no cf-ray, that doesn't tell you much, because plenty of sites run behind a different CDN or none at all.

Branding is where people overreach. Cloudflare's Error 403 page says a 403 without Cloudflare branding comes from the origin web server. Further down, the same page (updated 2026-09-28) says Cloudflare can serve an unstyled 403 itself, early in its infrastructure, before the domain's configuration loads; an SNI mismatch is its example, and those go unlogged. So I read the cf-ray and server headers, not the page design.

The body and Retry-After decide whether to try again

Our gateway's errors are empty. The 407 above was Content-Length: 0, and the 502 is too, with no reason given. Its documented causes: no exit for your targeting, a sticky session that couldn't be set up, a temporary network failure, a destination port other than 80 or 443, a client IP outside your allowlist. The last two are refused before we connect anywhere and aren't billed. I'd retry a 502 once, then check port and allowlist.

Our 503 reads 503 Service Temporarily Unavailable with Retry-After: 1, which means the gateway you reached is at its connection limit, so wait a second and send it again.

A site's 403 may carry a body; the Cloudflare one above ran to 27,613 bytes of HTML. Sometimes it names a number, and 1020 is worth knowing. Cloudflare's error 1020 page (updated 2026-10-14) says a security feature the site owner configured blocked the request, served as HTTP 403. The owner picked that rule, and the owner can tell you which one.

The same 403 page names WAF rules, Security Level, DDoS Protection, the Browser Integrity Check and Validation Checks as sources of its branded 403s. Your exit IP may be an input to any of them. It isn't the diagnosis.

Then Retry-After. A 429 is rate limiting, defined in RFC 6585 §4 rather than RFC 9110, and it may say how long to wait. Through one of our HTTPS tunnels a 429 is always the target's; Cloudflare documents sending one when its rate-limiting rules match. Our REST API at /api/v1 has its own 429 with Retry-After, and 403s like insufficient_scope, but that's account management, a different plane.

Nor is a 403 forever. RFC 9110 only says don't repeat it automatically with the same credentials. The same request forty more times is what doesn't help.

One script reads all four and names the layer

One request through the HTTP port, every header block kept, the headers that matter printed, the body searched for an error code, then a verdict. Change the three marked values. It reads port 7777 only; Premium and Mobile also take SOCKS5 on 1080, which it doesn't cover.

#!/usr/bin/env bash
# read-refusal.sh: capture every header block of one request through an HTTP proxy,
# then say which layer refused it.
set -u

PROXY_HOST="${PROXY_HOST:-premium.resifactory.net}"   # change: host from your line
PROXY_PORT="${PROXY_PORT:-7777}"                      # HTTP is 7777 on every pool
PROXY_USER="${PROXY_USER:-}"                          # change: USERNAME~us~a2d05335
PROXY_PASS="${PROXY_PASS:-}"                          # change: PASSWORD
URL="${1:?usage: read-refusal.sh https://the-page-that-failed/}"

dir="$(mktemp -d)"; : > "$dir/body"
auth=()
[ -n "$PROXY_USER" ] && auth=(-U "${PROXY_USER}:${PROXY_PASS}")

curl -sS --max-time 20 -x "http://${PROXY_HOST}:${PROXY_PORT}" ${auth[@]+"${auth[@]}"} \
  -D "$dir/headers" -o "$dir/body" "$URL"
echo "curl exit: $?"

# One block per status line. With two, the first is the hop.
tr -d '\r' < "$dir/headers" | awk '
  /^HTTP\// { n++; print ""; print "block " n ": " $0; next }
  tolower($0) ~ /^(proxy-authenticate|x-resifactory-request-id|server|cf-ray|retry-after|content-length):/ { print "  " $0 }
'
echo
echo "body bytes: $(wc -c < "$dir/body" | tr -d ' ')"
grep -oE 'error code:? *[0-9]{3,4}|Error [0-9]{4}' "$dir/body" 2>/dev/null | head -1

last="$(tr -d '\r' < "$dir/headers" | grep -E '^HTTP/' | tail -1 | awk '{print $2}')"
blocks="$(grep -cE '^HTTP/' "$dir/headers")"
h() { tr -d '\r' < "$dir/headers" | grep -qi "^$1:"; }

echo
if [ -z "$last" ]; then
  echo "verdict: no status line at all. Read curl's error above."
elif [ "$last" = "407" ] && h proxy-authenticate; then
  if h x-resifactory-request-id; then echo "verdict: our gateway asked for credentials. Fix the line, not the request."
  else echo "verdict: a proxy asked for credentials, and it did not sign as ours."; fi
elif [ "$blocks" -ge 2 ] && [ "$last" -lt 400 ]; then
  echo "verdict: the tunnel opened and the target answered $last. Nothing refused you."
elif [ "$blocks" -ge 2 ]; then
  echo "verdict: the tunnel opened; status $last is the target's."
  h cf-ray && echo "  Cloudflare's edge handled it; quote the cf-ray to the site owner."
  h retry-after && echo "  Retry-After is set: wait that long, then try once."
elif [ "$last" = "502" ] || [ "$last" = "503" ]; then
  echo "verdict: no tunnel. Status $last is the gateway's own."
else
  echo "verdict: one block, status $last. Plain http:// target? Then it may be either side."
fi

Run it with no credentials first: ./read-refusal.sh https://example.com/. Expect curl exit 56, one 407 block with Proxy-Authenticate and a request id, zero body bytes, and the "our gateway" verdict. That's the failing case, and it proves the script knows our signature. Then export PROXY_USER and PROXY_PASS and aim it at the page that's failing.

If you get a 407, fix the line: copy it again if there's been a reset, check the username fields, and top up that pool's wallet if it's empty (pricing has today's rate). If you get a 403 in the second block, the proxy is working and the site said no, so take the cf-ray to whoever runs the site and stop regenerating the list.

If you only remember one thing, count the status lines before you change the credential.

Try it on your own targets

Start small, then measure.

Top up a wallet from $0.50 and point it at your own targets, or try the free trial first.

See pricing