# Tunnel connection failed: 502 / 503 / 504 lab

This download reproduces `Tunnel connection failed: 502 Bad Gateway` (and 503, 504)
on your own machine. It backs the guide at
https://ipvolt.com/guides/fix-proxy-tunnel-502-503.

Everything runs on `127.0.0.1`. `lab.mjs` starts:

- an HTTPS and an HTTP origin for the name `origin.test`, signed by a throwaway CA
  that the lab creates with `openssl` and deletes afterwards; `/bad-gateway`
  answers `502 Bad Gateway` from the origin itself, through a working tunnel;
- a forward proxy that decides by target host name: `origin.test` is tunnelled,
  `refused.test` is dialled on a closed local port (the refusal becomes 502),
  `offline.test` answers 503 with `Retry-After: 5`, `slow.test` answers 504 once
  its 3-second upstream deadline expires, and `reset.test` closes the connection
  without a status line;
- a second proxy that answers 503 for a 4-second window on demand, used for a
  one-request-per-second timeline and for `retry_get.py`.

It then runs curl, Python Requests, HTTPX, aiohttp, Playwright (Chromium) and
Node.js `fetch` against every case, counts how many requests reached the proxy
and the origin, tries `curl --retry`, `--retry-all-errors` and urllib3's `Retry`,
and writes everything to a JSON file. No request leaves the machine.

Requirements: Node.js 24, `openssl`, curl, Python 3 with `requests`, `httpx` and
`aiohttp`, and `playwright` with Chromium installed (`npm install playwright`,
`npx playwright install chromium`).

```sh
node lab.mjs local-results.json
```

Optional environment variables: `PYTHON` (default `python3`), `CURLS`
(comma-separated curl binaries, default `curl`) and `CLIENTS` (a subset of
`curl,requests,httpx,aiohttp,playwright,fetch`).

Other files:

- `py_client.py`, `fetch_client.mjs`, `playwright_client.mjs`: one request per run
  with each client; they print one JSON line.
- `retry_get.py`: the guide's `get_with_retry()` helper, unchanged, with a small
  wrapper that logs each attempt.
- `wrong_port.mjs`: points the clients, as an HTTP proxy, at a SOCKS5-only
  listener of pproxy 2.7.9 (`pip install pproxy`). On Python 3.14, pproxy 2.7.9
  stops at startup with `RuntimeError: There is no current event loop`; we
  started it with
  `python -c "import asyncio, sys; asyncio.set_event_loop(asyncio.new_event_loop()); sys.argv[0] = 'pproxy'; from pproxy.server import main; main()" -l socks5://127.0.0.1:PORT`
  through a small wrapper passed as `PPROXY`.
- `node_checks.mjs`: the full Node.js `fetch` error object for a CONNECT 502, and
  how many CONNECTs a proxy that closes the connection receives within a bounded
  window, with and without `AbortSignal.timeout`.

Recorded runs (5 November 2026, Ubuntu 26.04.1):

- `results.json`: `lab.mjs` with curl 8.5.0 (Ubuntu 24.04 package
  8.5.0-2ubuntu10.16) and curl 8.18.0, Python 3.14.4 with Requests 2.34.2,
  urllib3 2.8.0, HTTPX 0.28.1, httpcore 1.0.9 and aiohttp 3.14.5, Node.js 24.22.1,
  Playwright 1.63.0 with Chromium 153.
- `wrong-port-results.json`: `wrong_port.mjs` with the same clients.
- `node-checks.json`: `node_checks.mjs` on Node.js 24.22.1 (undici 7.30.1).

Ports are random per run. The offline window and the 3-second deadline are the
lab's own settings; they say nothing about how long a real gateway or a mobile
IP rotation takes.
