DEV Community

98IP Proxy
98IP Proxy

Posted on Fully Autonomous

Test WebSockets Through a Proxy as a Lifecycle, Not a 101 Status

Disclosure: I work with 98IP. This post is a vendor-affiliated engineering guide and does not claim that every route or provider behaves identically.

A 101 Switching Protocols response proves that a WebSocket upgrade happened. It does not prove that the connection will carry messages in both directions, survive the real idle timeout, preserve the expected route on reconnect, or fail closed when the proxy disappears.

Treat a proxied WebSocket as a lifecycle with observable checkpoints.

1. Give every run a safe correlation ID

Record only operational metadata:

{
  "test_id": "ws-7f2a",
  "client": "browser-or-runtime-version",
  "proxy_protocol": "http-or-socks5",
  "gateway_label": "region-pool-a",
  "dns_owner": "gateway",
  "address_family": "ipv4",
  "expected_route": "proxy-only"
}
Enter fullscreen mode Exit fullscreen mode

Never log proxy passwords, authorization headers, cookies, reusable session tokens, or real message bodies. Use an endpoint you control or are authorized to test.

2. Instrument the whole lifecycle

In browser JavaScript you cannot send protocol-level ping frames directly, so use an application heartbeat when that is the environment under test:

const started = performance.now();
let lastAck = started;
let messagesIn = 0;
let messagesOut = 0;

const ws = new WebSocket(TEST_ENDPOINT, ["test.v1"]);

ws.addEventListener("open", () => {
  ws.send(JSON.stringify({ type: "probe", testId: TEST_ID }));
  messagesOut += 1;
});

ws.addEventListener("message", (event) => {
  messagesIn += 1;
  const message = JSON.parse(event.data);
  if (message.type === "heartbeat_ack") lastAck = performance.now();
});

const timer = setInterval(() => {
  const now = performance.now();
  if (now - lastAck > HEARTBEAT_DEADLINE_MS) {
    clearInterval(timer);
    ws.close(4000, "heartbeat deadline exceeded");
    return;
  }
  ws.send(JSON.stringify({ type: "heartbeat", testId: TEST_ID }));
  messagesOut += 1;
}, HEARTBEAT_INTERVAL_MS);

ws.addEventListener("close", (event) => {
  console.log({
    testId: TEST_ID,
    durationMs: Math.round(performance.now() - started),
    messagesIn,
    messagesOut,
    closeCode: event.code,
    clean: event.wasClean
  });
});
Enter fullscreen mode Exit fullscreen mode

Use performance.now() for durations; it is monotonic. Keep wall-clock timestamps only to correlate client, proxy, and server logs.

3. Prove both directions

An open event is not a bidirectional test.

  1. Send a unique client probe and require the controlled server to acknowledge it.
  2. Have the server send a separate message that the client validates.
  3. Record direction, time, byte count, and a harmless content hash.

Start at concurrency one. If server-to-client traffic fails while client-to-server works, investigate buffering, subscription state, worker lifecycle, and idle controls before rotating the exit.

4. Find the effective idle cutoff

Run the same validated flow with application-idle intervals of 15, 30, 60, 120, and 300 seconds. Probe after each interval. Repeat the first failing boundary several times on the same exit cohort.

Choose a heartbeat interval below the lowest repeatable cutoff with margin for jitter and event-loop delay. Avoid an unnecessarily short interval: at fleet scale, heartbeat traffic becomes real load.

5. Test reconnect policy explicitly

Do not mix these two cases:

  • Sticky reconnect: reuse the intended session key and check whether the promised identity scope—IP, region, or ASN—remains stable.
  • Fresh reconnect: request a new session and verify that the application recovers correctly on a new exit.

Your assertion must match the product contract. “Same country” is not “same IP.”

Then inject controlled failures: clean server close, abrupt disconnect, proxy refusal, target refusal, DNS failure, and a small concurrent reconnect burst. For every case, define the expected close code, backoff, maximum attempts, session policy, and maximum recovery time.

6. Add a fail-closed route test

Make the configured proxy unavailable. The expected result is a visible failure through the intended route—not a silent direct connection.

Correlate the client test ID with proxy-side evidence. A client-side “connected” state alone cannot prove which path was used.

Minimum release matrix

Case Evidence Pass condition
Upgrade status, subprotocol, route ID valid upgrade through intended gateway
Client → server acknowledgement unique probe received once
Server → client client validation independent message received before deadline
Idle stepped intervals cutoff measured and repeatable
Heartbeat send/ack times timeout closes or quarantines socket
Sticky reconnect exit-cohort evidence promised identity scope preserved
Fresh reconnect new-session evidence recovery works on a new route
Proxy unavailable route observation no direct fallback
IPv4/IPv6 family-specific run expected behavior documented

The result should be a reproducible record, not a screenshot of a green connection badge.

More testing and routing guides from the team I work with: https://en.98ip.com/?k=dev

``

Top comments (0)