[{"data":1,"prerenderedAt":20},["ShallowReactive",2],{"blog:post:en:testing-http3-quic-with-proxies":3},{"slug":4,"lang":5,"title":6,"summary":7,"date":8,"tags":9,"tag_slugs":15,"thumbnail_url":16,"translations":17,"body":18,"asset_base":19},"testing-http3-quic-with-proxies","en","Testing HTTP/3 (QUIC) Connectivity Through Proxies","Learn how to verify HTTP/3 support and graceful fallback to HTTP/1.1 by routing client traffic through proxies, capturing packets, and measuring latency for reliable API performance testing.","2026-10-13",[10,11,12,13,14],"http3","proxies","performance","networking","curl",[10,11,12,13,14],"https://blog-api.ro-proxy.com/api/blog/posts/testing-http3-quic-with-proxies/thumbnail.svg?lang=en",[5],"## Why Test HTTP/3 Behind a Proxy\n\nModern APIs increasingly advertise HTTP/3 (QUIC) as the preferred transport, falling back to HTTP/2 or HTTP/1.1 when QUIC is blocked. When your traffic passes through a corporate proxy, a cloud‑based gateway, or a residential proxy pool, the proxy may terminate TLS, interfere with UDP packets, or force TCP fallback. Verifying that your client still negotiates the correct version and experiences acceptable latency is essential for performance‑critical services such as real‑time bidding, multiplayer game backends, or streaming ingest.\n\nTesting HTTP/3 through a proxy lets you answer three concrete questions:\n\n1. Does the proxy allow UDP traffic on the QUIC port (usually 443) and preserve the QUIC connection IDs?\n2. If UDP is blocked, does the client correctly fall back to TCP‑based HTTP/2 or HTTP/1.1 without application‑level errors?\n3. What is the added latency introduced by the proxy for each protocol version, and does it affect your SLA?\n\nBy automating these checks you can catch mis‑configured proxy rules before they degrade user experience in production.\n\n## Prerequisites\n\n- A proxy that supports both TCP and UDP forwarding. Most residential, datacenter, or SOCKS5 proxies meet this requirement; HTTP CONNECT proxies only forward TCP, so UDP‑based QUIC will fail and trigger fallback.\n- A test endpoint that speaks HTTP/3. For demonstration we use the public test server at `https://quic.rocks:443` (replace with your own API URL).\n- Tools: `curl` compiled with nghttp3 support, `ngtcp2`’s `nghttp` client, or a Node.js script using the `undici` library (which auto‑selects HTTP/3 when available).\n- Optional: Wireshark or `tcpdump` to inspect packets.\n\nSet environment variables for your proxy (replace the placeholders with the values supplied by your provider):\n\n```bash\nexport PROXY_HOST=\"\u003CPROXY_HOST>\"\nexport PROXY_PORT=\"\u003CPROXY_PORT>\"\nexport PROXY_USER=\"\u003CPROXY_USER>\"\nexport PROXY_PASS=\"\u003CPROXY_PASS>\"\nexport TEST_URL=\"https://quic.rocks:443\"  # or your internal API\n```\n\n## Verifying UDP Pass‑Through with curl\n\nCurl built against nghttp3 can speak HTTP/3 over UDP. When a proxy is specified with `-x`, curl treats the proxy as an HTTP CONNECT tunnel for TCP; UDP is not tunneled, so the request will fall back. To force UDP we use the `--proxy-insecure` flag together with `--proxy` pointing to a SOCKS5 proxy, which does forward UDP.\n\nFirst, test direct connectivity (no proxy) to confirm the server speaks QUIC:\n\n```bash\ncurl --http3 -v \"$TEST_URL\" 2>&1 | grep -E \"HTTP/3|ALPN\"\n```\n\nYou should see lines like `* Connected to quic.rocks (2a00:1450:4001:812::200e) port 443 (#0)` and `* Using HTTP/3`.\n\nNow route through a SOCKS5 proxy that supports UDP:\n\n```bash\ncurl --http3 --proxy socks5h://$PROXY_USER:$PROXY_PASS@$PROXY_HOST:$PROXY_PORT -v \"$TEST_URL\" 2>&1 | grep -E \"HTTP/3|ALPN|Connected\"\n```\n\nIf the proxy forwards UDP, the output will still show `* Using HTTP/3`. If UDP is blocked, curl will retry over TCP and you will see `* Using HTTP/2` or `* Using HTTP/1.1`.\n\n### Measuring Latency\n\nAdd `-w \"%{time_total}\"` to capture the total transaction time:\n\n```bash\ncurl --http3 --proxy socks5h://$PROXY_USER:$PROXY_PASS@$PROXY_HOST:$PROXY_PORT -s -o /dev/null -w \"%{time_total}\" \"$TEST_URL\"\n```\n\nRecord the value for both direct and proxied runs; the difference approximates the proxy‑induced overhead for QUIC.\n\n## Testing with nghttp (ngtcp2) for Granular Insight\n\nThe `nghttp` client from the ngtcp2 suite lets you specify the exact transport and log QUIC parameters.\n\nInstall ngtcp2 (e.g., `sudo apt-get install ngtcp2` on Ubuntu) and run:\n\n```bash\nnghttp -v --proxy=$PROXY_HOST:$PROXY_PORT --proxy-user=$PROXY_USER --proxy-pass=$PROXY_PASS https://quic.rocks:443/\n```\n\nLook for lines such as:\n\n- `[info] connected to proxy \u003Chost>:\u003Cport>`\n- `[info] ALPN offered: h3`\n- `[info] ALPN selected: h3` (means QUIC succeeded)\n\nIf the proxy does not forward UDP, you will see a fallback message like `[warn] UDP send failed, falling back to TCP`.\n\nYou can also extract the RTT from the statistics block at the end:\n\n```\n[info] RTT: 23.4 ms\n```\n\nCompare the RTT with and without the proxy to quantify the added delay.\n\n## Node.js Validation with undici\n\nUndici is Node.js’s native HTTP client; it automatically attempts HTTP/3 when the underlying system supports QUIC (via the `undici` resolver). The following script tests both direct and proxied connections, logs the negotiated protocol, and measures request time.\n\nCreate `http3-test.js`:\n\n```js\nimport { ProxyAgent, Dispatcher } from 'undici';\n\nasync function test(url, proxyOpts = null) {\n  let dispatcher;\n  if (proxyOpts) {\n    // Undici uses ProxyAgent for HTTP CONNECT proxies (TCP only).\n    // For SOCKS5 we rely on the system’s socks proxy via environment variables.\n    dispatcher = new ProxyAgent({\n      uri: `http://${proxyOpts.host}:${proxyOpts.port}`,\n      headers: { 'proxy-authorization': `Basic ${Buffer.from(`${proxyOpts.user}:${proxyOpts.pass}`).toString('base64')` },\n    });\n  } else {\n    dispatcher = new Dispatcher();\n  }\n\n  const start = process.hrtime.bigint();\n  const { statusCode, headers } = await dispatcher.request({ path: new URL(url).pathname + new URL(url).search, method: 'GET' });\n  const end = process.hrtime.bigint();\n  const ms = Number(end - start) / 1e6;\n\n  // Undici does not expose the negotiated version directly; infer from Alt‑Svc header\n  const altSvc = headers['alt-svc'] || '';\n  const usesH3 = altSvc.includes('h3=');\n\n  console.log({\n    url,\n    proxy: proxyOpts ? `${proxyOpts.host}:${proxyOpts.port}` : 'direct',\n    statusCode,\n    usesH3,\n    latencyMs: Math.round(ms, 2),\n  });\n}\n\n(async () => {\n  const TEST_URL = process.env.TEST_URL || 'https://quic.rocks:443';\n  await test(TEST_URL); // direct\n  await test(TEST_URL, {\n    host: process.env.PROXY_HOST,\n    port: process.env.PROXY_PORT,\n    user: process.env.PROXY_USER,\n    pass: process.env.PROXY_PASS,\n  });\n})();\n```\n\nRun with:\n\n```bash\nnode http3-test.js\n```\n\nSample output:\n\n```\n{ url: 'https://quic.rocks:443',\n  proxy: 'direct',\n  statusCode: 200,\n  usesH3: true,\n  latencyMs: 42 }\n{ url: 'https://quic.rocks:443',\n  proxy: '123.45.67.89:1080',\n  statusCode: 200,\n  usesH3: false,\n  latencyMs: 78 }\n```\n\nIn this example the proxy blocked UDP, forcing a fallback to HTTP/2 (indicated by `usesH3: false`) and added roughly 36 ms of latency.\n\n## Automating the Matrix in CI\n\nCombine the three approaches into a single CI step that fails if any of the following conditions occur:\n\n- Expected HTTP/3 is not negotiated when a direct connection is used (indicates server mis‑configuration).\n- When a proxy is defined, HTTP/3 is negotiated **only** if the proxy is known to support UDP forwarding; otherwise the test should expect a fallback and verify that the fallback protocol is either HTTP/2 or HTTP/1.1.\n- Latency increase exceeds a threshold (e.g., 50 ms for QUIC, 30 ms for HTTP/2).\n\nA simplified GitHub Actions workflow:\n\n```yaml\nname: HTTP/3 Proxy Validation\non:\n  push:\n    branches: [main]\n  pull_request:\n\njobs:\n  http3-proxy-check:\n    runs-on: ubuntu-latest\n    env:\n      TEST_URL: https://quic.rocks:443\n      PROXY_HOST: ${{ secrets.PROXY_HOST }}\n      PROXY_PORT: ${{ secrets.PROXY_PORT }}\n      PROXY_USER: ${{ secrets.PROXY_USER }}\n      PROXY_PASS: ${{ secrets.PROXY_PASS }}\n    steps:\n      - uses: actions/checkout@v4\n      - name: Install dependencies\n        run: |\n          sudo apt-get update\n          sudo apt-get install -y curl ngtcp2-client\n          npm ci\n      - name: Direct QUIC check\n        run: |\n          curl --http3 -s -o /dev/null -w \"%%{time_total}\" $TEST_URL | awk '{ if ($1 \u003C 0.1) exit 1 }'\n      - name: Proxy QUIC check (expect fallback if proxy blocks UDP)\n        run: |\n          curl --http3 --proxy socks5h://$PROXY_USER:$PROXY_PASS@$PROXY_HOST:$PROXY_PORT -s -o /dev/null -w \"%%{time_total}\" $TEST_URL | awk '{ if ($1 > 0.08) exit 1 }'\n      - name: Undici validation\n        run: node http3-test.js\n```\n\nAdjust the thresholds to match your network baseline.\n\n## Troubleshooting Common Issues\n\n| Symptom                                 | Likely Cause                                   | Fix                                                                                     |\n|-----------------------------------------|-----------------------------------------------|----------------------------------------------------------------------------------------|\n| `curl: (0) Failed to connect to proxy` | Proxy host/port wrong or authentication missing| Verify `$PROXY_HOST`, `$PROXY_PORT`, and that the proxy allows CONNECT to port 443.   |\n| No `ALPN` line in curl output           | Proxy stripping ALPN or using HTTP CONNECT only| Use a SOCKS5 proxy (`socks5h://`) which forwards TLS payload unchanged.                |\n| `nghttp` reports `UDP send failed`      | Proxy blocks UDP or NAT timeout               | Switch to a proxy that explicitly allows UDP, or test TCP fallback behavior.          |\n| Undici reports `usesH3: false` despite direct success | System lacks QUIC support (missing libnghttp3) | Install a recent version of Node (≥18) with `--experimental-websocket` or use a patched undici build. |\n| High latency (>150 ms) only through proxy | Proxy overloaded or geographic distance       | Choose a proxy location closer to the test server or enable keep‑alive/reuse connections. |\n\n## Conclusion\n\nTesting HTTP/3 (QUIC) through proxies is a lightweight yet powerful way to guarantee that your APIs retain their performance advantages even when traffic traverses intermediary nodes. By:\n\n1. Confirming UDP pass‑through with curl or nghttp,\n2. Measuring latency differences for each protocol version,\n3. Validating automatic fallback with Node.js’ undici,\n4. Embedding the checks in CI with clear pass/fail criteria,\n\nyou gain confidence that proxy configurations will not silently degrade user experience. The same methodology applies to any UDP‑based protocol (e.g., DNS over QUIC, MASQUE) and can be extended to monitor real‑world traffic patterns using tools like `tcpdump` or `iftop`. Start with a single proxy location, establish baseline latencies, then expand to a multi‑region proxy matrix to catch geography‑specific quirks before they reach your users.\n","https://blog-api.ro-proxy.com/api/blog/posts/testing-http3-quic-with-proxies/assets",1792303105253]