Testing HTTP/3 (QUIC) Connectivity Through Proxies
October 13, 2026
Why Test HTTP/3 Behind a Proxy
Modern 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.
Testing HTTP/3 through a proxy lets you answer three concrete questions:
- Does the proxy allow UDP traffic on the QUIC port (usually 443) and preserve the QUIC connection IDs?
- If UDP is blocked, does the client correctly fall back to TCP‑based HTTP/2 or HTTP/1.1 without application‑level errors?
- What is the added latency introduced by the proxy for each protocol version, and does it affect your SLA?
By automating these checks you can catch mis‑configured proxy rules before they degrade user experience in production.
Prerequisites
- 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.
- 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). - Tools:
curlcompiled with nghttp3 support,ngtcp2’snghttpclient, or a Node.js script using theundicilibrary (which auto‑selects HTTP/3 when available). - Optional: Wireshark or
tcpdumpto inspect packets.
Set environment variables for your proxy (replace the placeholders with the values supplied by your provider):
export PROXY_HOST="<PROXY_HOST>"
export PROXY_PORT="<PROXY_PORT>"
export PROXY_USER="<PROXY_USER>"
export PROXY_PASS="<PROXY_PASS>"
export TEST_URL="https://quic.rocks:443" # or your internal API
Verifying UDP Pass‑Through with curl
Curl 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.
First, test direct connectivity (no proxy) to confirm the server speaks QUIC:
curl --http3 -v "$TEST_URL" 2>&1 | grep -E "HTTP/3|ALPN"
You should see lines like * Connected to quic.rocks (2a00:1450:4001:812::200e) port 443 (#0) and * Using HTTP/3.
Now route through a SOCKS5 proxy that supports UDP:
curl --http3 --proxy socks5h://$PROXY_USER:$PROXY_PASS@$PROXY_HOST:$PROXY_PORT -v "$TEST_URL" 2>&1 | grep -E "HTTP/3|ALPN|Connected"
If 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.
Measuring Latency
Add -w "%{time_total}" to capture the total transaction time:
curl --http3 --proxy socks5h://$PROXY_USER:$PROXY_PASS@$PROXY_HOST:$PROXY_PORT -s -o /dev/null -w "%{time_total}" "$TEST_URL"
Record the value for both direct and proxied runs; the difference approximates the proxy‑induced overhead for QUIC.
Testing with nghttp (ngtcp2) for Granular Insight
The nghttp client from the ngtcp2 suite lets you specify the exact transport and log QUIC parameters.
Install ngtcp2 (e.g., sudo apt-get install ngtcp2 on Ubuntu) and run:
nghttp -v --proxy=$PROXY_HOST:$PROXY_PORT --proxy-user=$PROXY_USER --proxy-pass=$PROXY_PASS https://quic.rocks:443/
Look for lines such as:
[info] connected to proxy <host>:<port>[info] ALPN offered: h3[info] ALPN selected: h3(means QUIC succeeded)
If the proxy does not forward UDP, you will see a fallback message like [warn] UDP send failed, falling back to TCP.
You can also extract the RTT from the statistics block at the end:
[info] RTT: 23.4 ms
Compare the RTT with and without the proxy to quantify the added delay.
Node.js Validation with undici
Undici 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.
Create http3-test.js:
import { ProxyAgent, Dispatcher } from 'undici';
async function test(url, proxyOpts = null) {
let dispatcher;
if (proxyOpts) {
// Undici uses ProxyAgent for HTTP CONNECT proxies (TCP only).
// For SOCKS5 we rely on the system’s socks proxy via environment variables.
dispatcher = new ProxyAgent({
uri: `http://${proxyOpts.host}:${proxyOpts.port}`,
headers: { 'proxy-authorization': `Basic ${Buffer.from(`${proxyOpts.user}:${proxyOpts.pass}`).toString('base64')` },
});
} else {
dispatcher = new Dispatcher();
}
const start = process.hrtime.bigint();
const { statusCode, headers } = await dispatcher.request({ path: new URL(url).pathname + new URL(url).search, method: 'GET' });
const end = process.hrtime.bigint();
const ms = Number(end - start) / 1e6;
// Undici does not expose the negotiated version directly; infer from Alt‑Svc header
const altSvc = headers['alt-svc'] || '';
const usesH3 = altSvc.includes('h3=');
console.log({
url,
proxy: proxyOpts ? `${proxyOpts.host}:${proxyOpts.port}` : 'direct',
statusCode,
usesH3,
latencyMs: Math.round(ms, 2),
});
}
(async () => {
const TEST_URL = process.env.TEST_URL || 'https://quic.rocks:443';
await test(TEST_URL); // direct
await test(TEST_URL, {
host: process.env.PROXY_HOST,
port: process.env.PROXY_PORT,
user: process.env.PROXY_USER,
pass: process.env.PROXY_PASS,
});
})();
Run with:
node http3-test.js
Sample output:
{ url: 'https://quic.rocks:443',
proxy: 'direct',
statusCode: 200,
usesH3: true,
latencyMs: 42 }
{ url: 'https://quic.rocks:443',
proxy: '123.45.67.89:1080',
statusCode: 200,
usesH3: false,
latencyMs: 78 }
In this example the proxy blocked UDP, forcing a fallback to HTTP/2 (indicated by usesH3: false) and added roughly 36 ms of latency.
Automating the Matrix in CI
Combine the three approaches into a single CI step that fails if any of the following conditions occur:
- Expected HTTP/3 is not negotiated when a direct connection is used (indicates server mis‑configuration).
- 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.
- Latency increase exceeds a threshold (e.g., 50 ms for QUIC, 30 ms for HTTP/2).
A simplified GitHub Actions workflow:
name: HTTP/3 Proxy Validation
on:
push:
branches: [main]
pull_request:
jobs:
http3-proxy-check:
runs-on: ubuntu-latest
env:
TEST_URL: https://quic.rocks:443
PROXY_HOST: ${{ secrets.PROXY_HOST }}
PROXY_PORT: ${{ secrets.PROXY_PORT }}
PROXY_USER: ${{ secrets.PROXY_USER }}
PROXY_PASS: ${{ secrets.PROXY_PASS }}
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y curl ngtcp2-client
npm ci
- name: Direct QUIC check
run: |
curl --http3 -s -o /dev/null -w "%%{time_total}" $TEST_URL | awk '{ if ($1 < 0.1) exit 1 }'
- name: Proxy QUIC check (expect fallback if proxy blocks UDP)
run: |
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 }'
- name: Undici validation
run: node http3-test.js
Adjust the thresholds to match your network baseline.
Troubleshooting Common Issues
| Symptom | Likely Cause | Fix |
|---|---|---|
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. |
No ALPN line in curl output |
Proxy stripping ALPN or using HTTP CONNECT only | Use a SOCKS5 proxy (socks5h://) which forwards TLS payload unchanged. |
nghttp reports UDP send failed |
Proxy blocks UDP or NAT timeout | Switch to a proxy that explicitly allows UDP, or test TCP fallback behavior. |
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. |
| 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. |
Conclusion
Testing 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:
- Confirming UDP pass‑through with curl or nghttp,
- Measuring latency differences for each protocol version,
- Validating automatic fallback with Node.js’ undici,
- Embedding the checks in CI with clear pass/fail criteria,
you 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.