Before interpreting a 403, 407, 429, or timeout, ask one question: did the client record an HTTP response? The first three are protocol results. A cURL exit 28 or Playwright timeout is a client observation. Those facts point to different layers, and mixing them under one failed label discards the best evidence you have.
A status code is evidence, not attribution.
A response means some HTTP responder wrote back. It does not automatically name that responder. A failure without a status means the client stopped without recording response headers. It does not automatically name the slow or broken hop.
Trace the signal before naming the cause
Use the local decision tree to preserve exactly what you observed. It asks whether a response arrived, which exchange produced it, and what the client reported when no status was available. The result stays deliberately narrower than a root-cause verdict.
Place the first thing you actually observed.
This local classifier separates an HTTP response from a client-side failure before it suggests a layer.
There is not enough evidence to name a failing layer. Layer: Evidence collection. Evidence: insufficient evidence.
There is not enough evidence to name a failing layer.
No HTTP status or normalized client failure has been recorded.
This does not prove
- That the proxy, destination, client, or local network caused the failure.
- That no HTTP response existed. It may simply be absent from the capture.
Capture next
- Record whether an HTTP status appeared at any point.
- Record the client name, normalized error category, and event order.
- Keep a timestamp and a sanitized request label. Omit URLs, IP addresses, and credentials.
Nothing leaves this page. Selections stay in this browser tab and are not saved or sent. Do not paste URLs, IP addresses, headers, or credentials into a diagnostic note.
What each result actually says
403: a responder refused the request
RFC 9110 defines 403 (opens in a new tab) as a response from a server that understood the request and refused to fulfill it. That establishes refusal. It does not identify the origin application, CDN, gateway, or another destination-facing component, and it does not prove bot detection, bad proxy credentials, or a dead proxy.
If cURL response_code or a Playwright Response records 403 after a successful CONNECT, the strongest safe reading is destination-facing HTTP refusal. Preserve the CONNECT code, redirect chain, timestamp, sanitized response fingerprint, client version, and any permitted correlation ID. Use owner or provider logs to go further.
407: a proxy is asking for authentication
RFC 9110 defines 407 (opens in a new tab) as a proxy-authentication challenge. A conforming response includes Proxy-Authenticate. This is the clearest proxy-layer signal in this group, but it still does not prove that the password alone is wrong. Credentials may be absent, rejected, expired, incomplete, or offered with an unsupported scheme.
Record the challenge scheme and realm, proxy endpoint alias, proxy scheme, CONNECT code, and whether credentials were configured. Never record the raw credentials in your evidence row.
429: a responder reports rate limiting
RFC 6585 defines 429 (opens in a new tab) as too many requests in a given period. The specification does not define how the responder identifies a user or counts requests. A 429 does not reveal whether the counter is tied to an IP, account, cookie, resource, or something else. Retry-After is optional.
Capture Retry-After when present, the authorized request cadence, a sanitized response fingerprint, and the relevant non-secret session or account alias. Pause and follow the site owner's limits instead of turning the response into a retry experiment.
408 and 504: timeout responses
An HTTP 408 (opens in a new tab) or 504 (opens in a new tab) is still a received response. A 408 says the responding server did not receive a complete request within its wait window. A 504 says a gateway or proxy did not receive a timely response from an upstream server. Neither should be collapsed into a local client timeout.
CONNECT status and transfer status are different
For HTTPS through an HTTP proxy, the client first asks the proxy to open a CONNECT tunnel. A 2xx CONNECT response switches that connection into tunnel mode. Any non-2xx CONNECT response means the tunnel was not formed, although the status alone may not explain the proxy's decision.
| Observation | What it establishes | What stays unknown |
|---|---|---|
| http_connect = 407 | The proxy challenged tunnel authentication | Which credential or account condition caused the challenge |
| http_connect = 2xx, response_code = 403 | The tunnel formed and a later HTTP responder refused | Which destination-facing component refused and why |
| http_connect = 2xx, cURL exit = 28 | The tunnel formed before cURL's configured deadline expired | Which later phase consumed the remaining time |
| Phase unknown | A status was captured without enough exchange context | Whether it came from CONNECT or the requested transfer |
- Observation
- http_connect = 407
- What it establishes
- The proxy challenged tunnel authentication
- What stays unknown
- Which credential or account condition caused the challenge
- Observation
- http_connect = 2xx, response_code = 403
- What it establishes
- The tunnel formed and a later HTTP responder refused
- What stays unknown
- Which destination-facing component refused and why
- Observation
- http_connect = 2xx, cURL exit = 28
- What it establishes
- The tunnel formed before cURL's configured deadline expired
- What stays unknown
- Which later phase consumed the remaining time
- Observation
- Phase unknown
- What it establishes
- A status was captured without enough exchange context
- What stays unknown
- Whether it came from CONNECT or the requested transfer
cURL exposes the CONNECT response as http_connect and the requested transfer response as response_code in write-out (opens in a new tab). The CONNECT specification (opens in a new tab) defines the tunnel boundary. Capture both fields whenever an HTTPS proxy is involved.
Capture the split in each client
# TARGET_URL and proxy settings stay in your local environment.
curl --silent --show-error --output /dev/null \
--connect-timeout 10 --max-time 30 \
--write-out $'exit=%{exitcode}\nconnect=%{http_connect}\nresponse=%{response_code}\nproxy=%{proxy_used}\nconnect_s=%{time_connect}\nfirst_byte_s=%{time_starttransfer}\ntotal_s=%{time_total}\n' \
"$TARGET_URL"cURL exit 28 means its configured operation deadline was reached. It does not identify the stage. Playwright likewise treats HTTP error responses as HTTP-complete results and exposes client or transport failures through requestfailed. See the official cURL error list (opens in a new tab) and Playwright request lifecycle (opens in a new tab).
Send a ticket that can be investigated
- coarse timestamp and timezone, client and browser-engine versions, and repeat count;
- proxy endpoint alias and scheme, plus a boolean for whether credentials were configured;
- CONNECT code, transfer response code, and normalized client error as separate fields;
- timeout kind, configured deadline, actual elapsed time, and available phase timings;
- Retry-After, response-header names, and a sanitized body fingerprint when permitted; and
- the strongest supported layer, what remains unknown, and the first controlled comparison that diverged.
If the clients disagree, use the cURL vs. Playwright four-cell diagnostic. If requests complete but the exit identity changes, open the sticky-session recorder. If the deadline is the only signal, decompose it with the latency worksheet.
Provider claims belong beside current rankings, underlying reports, and the public methodology. Follow the site owner's instructions and the responsible-use boundary whenever an access response tells you to stop.