Testing WebSocket Connections with Bash: Handshake, Ping/Pong and CI
AI generated
$_
#!/
Bash · Networking · WebSocket · APIs
Testing WebSocket Connections with Bash
Handshake, ping/pong and CI without a browser

Anyone who only checks WebSocket endpoints in the browser with DevTools loses reproducibility and automatability. With openssl, curl and websocat, you can test WebSocket connections with Bash, walk through the handshake by hand, and wire connection checks straight into the CI pipeline.

17 min read websocat · openssl s_client · curl upgrade Bash 4.x/5.x · Linux · CI/CD

1. Why WebSocket tests differ from HTTP requests

A normal HTTP request is finished as soon as the response arrives. A WebSocket connection, on the other hand, stays open, sends and receives frames in both directions, and often lives for minutes or hours. Anyone who wants to test WebSocket connections with Bash therefore has to check more than a single request: the handshake, the data exchange, keep-alive behavior, and a clean connection teardown over the entire lifetime of the connection.

Classic tools like curl without extra options fail here because they are built for request-response cycles, not for long-lived, bidirectional streams. That is exactly why it pays off to test WebSocket connections with Bash in a targeted way: a shell script can trigger the handshake, verify the upgrade to the WebSocket protocol, send frames, and validate the response, entirely headless and reproducible in any pipeline.

In practice this affects chat systems, live price feeds, notification services, and Magento extensions with real-time updates in the admin area. A dropped WebSocket connection often only shows up in the browser through missing feedback. An automated test that checks WebSocket connections with Bash catches the problem before the user does.

2. Tools: openssl, curl and websocat at a glance

Three tools cover most use cases when you want to test WebSocket connections with Bash. openssl s_client establishes a raw TLS or TCP connection and lets you send the HTTP upgrade request by hand. That is tedious but shows every byte of the protocol and is excellent for debugging handshake problems.

Modern curl versions (from 7.86) support WebSocket experimentally via curl --http1.1 -N with the right upgrade headers, but that is not always available for production test scripts since many distributions ship older curl builds. The third and, in practice, most important tool is websocat, a single static binary that treats a WebSocket connection like a pipe: stdin goes in, stdout comes out. That lets you test WebSocket connections with Bash without writing a single line of handshake code yourself.

For CI environments, websocat is ideal because it has no runtime dependencies and can be copied into a Docker image as a single binary file. For quick debugging on a production server where installing websocat is not allowed, openssl s_client remains the fallback, because openssl is available practically everywhere.


#!/usr/bin/env bash
# check-tools.sh — verify which WebSocket testing tools are available
set -euo pipefail

check_tool() {
  local name="$1"
  if command -v "$name" &>/dev/null; then
    echo "[OK] $name found: $(command -v "$name")"
    return 0
  fi
  echo "[MISSING] $name not found" >&2
  return 1
}

echo "=== WebSocket toolchain check ==="
check_tool openssl || true
check_tool curl || true
check_tool websocat || echo "  Install: cargo install websocat OR download static binary"

3. Rebuilding the WebSocket handshake by hand

The WebSocket handshake is a normal HTTP request with one decisive difference: the header Upgrade: websocket together with Connection: Upgrade and a random Base64 value in Sec-WebSocket-Key. The server responds with status code 101 Switching Protocols and a computed Sec-WebSocket-Accept, built from the sent key plus a fixed GUID via SHA1. Anyone who rebuilds this flow by hand once understands WebSocket connections in Bash far better afterward than from any documentation.

With openssl s_client -connect host:443 -quiet you open the TLS connection, then write the upgrade request including all mandatory headers into it, and read the response. Per the specification, the Sec-WebSocket-Key must consist of 16 random bytes, Base64-encoded. Bash produces that conveniently with openssl rand -base64 16.


#!/usr/bin/env bash
# manual-handshake.sh — build a raw WebSocket upgrade request by hand
set -euo pipefail

HOST="echo.websocket.org"
PORT=443
WS_KEY=$(openssl rand -base64 16)

# Build the upgrade request as a here-doc, CRLF line endings required
request=$(printf 'GET / HTTP/1.1\r\nHost: %s\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: %s\r\nSec-WebSocket-Version: 13\r\n\r\n' "$HOST" "$WS_KEY")

echo "[INFO] Sending handshake with Sec-WebSocket-Key: $WS_KEY"

# Send request through a TLS connection and capture the first response lines
response=$(printf '%s' "$request" | timeout 5 openssl s_client -connect "$HOST:$PORT" -quiet 2>/dev/null | head -n 10)

if echo "$response" | grep -q "101 Switching Protocols"; then
  echo "[OK] Server upgraded the connection to WebSocket"
else
  echo "[FAIL] Handshake did not succeed:" >&2
  echo "$response" >&2
  exit 1
fi

This script only checks the handshake, not the data exchange afterward, but that is already enough to determine whether a reverse proxy or a firewall is blocking the upgrade. Anyone who wants to test WebSocket connections with Bash should keep this building block isolated from the rest, because handshake failures and data transfer failures have completely different root causes.

4. Using websocat as a native Bash testing tool

Instead of building the handshake and frame format by hand, websocat takes care of both and afterward behaves like a bidirectional pipe endpoint. A simple call echo "ping" | websocat wss://example.com/ws opens the connection, sends the message as a text frame, and prints every received response to stdout. That makes it trivial to test WebSocket connections with Bash without worrying about frame masking, opcode bytes, or fragmentation.

For automated tests you combine websocat with timeout so a script does not wait forever for a response that never arrives. The option -n1 stops websocat after the first received frame, which is ideal for simple request-response style WebSocket APIs (such as JSON-RPC over WebSocket). For streaming endpoints that continuously send data, you instead read a fixed number of lines with head -n N.


#!/usr/bin/env bash
# ws-test.sh — send a message and validate the WebSocket response with websocat
set -euo pipefail

WS_URL="${1:?Usage: ws-test.sh <ws-url> <message>}"
MESSAGE="${2:?Missing message payload}"
TIMEOUT_SECONDS=5

response=$(echo "$MESSAGE" | timeout "$TIMEOUT_SECONDS" websocat -n1 "$WS_URL" 2>/dev/null) || {
  echo "[FAIL] No response within ${TIMEOUT_SECONDS}s or connection error" >&2
  exit 1
}

if [[ -z "$response" ]]; then
  echo "[FAIL] Empty response from $WS_URL" >&2
  exit 1
fi

echo "[OK] Response: $response"

# Validate the response is well-formed JSON, if the API returns JSON
if command -v jq &>/dev/null; then
  echo "$response" | jq -e . >/dev/null 2>&1 && echo "[OK] Response is valid JSON"
fi

5. Verifying ping/pong frames and connection stability

WebSocket defines its own control frames for ping and pong, independent of the actual payload. A server periodically sends a ping frame, the client must answer with a pong frame, otherwise the server considers the connection dead and closes it. Anyone who wants to test WebSocket connections with Bash should therefore check whether the client itself or the tested server correctly reacts to these control frames, because this is exactly where many implementations silently lose the connection after longer periods of inactivity.

websocat supports automatic ping with the option --ping-interval, which is excellent for long-running tests: you open a connection, keep it open for several minutes with active ping/pong, and afterward check whether it still responds. That uncovers problems a single short test would never find, such as load balancers that drop idle connections after 60 seconds without traffic.


#!/usr/bin/env bash
# ws-stability.sh — keep a WebSocket connection open and verify ping/pong survival
set -euo pipefail

WS_URL="${1:?Usage: ws-stability.sh <ws-url> <duration-seconds>}"
DURATION="${2:-120}"

echo "[INFO] Holding connection open for ${DURATION}s with automatic ping every 20s"

# --ping-interval keeps the connection alive; -E exits on first error
if timeout "$((DURATION + 5))" websocat --ping-interval 20 -E "$WS_URL" < /dev/null > /tmp/ws-session.log 2>&1; then
  echo "[OK] Connection survived ${DURATION}s of ping/pong"
else
  exit_code=$?
  echo "[FAIL] Connection dropped or errored after less than ${DURATION}s (exit $exit_code)" >&2
  tail -n 5 /tmp/ws-session.log >&2
  exit 1
fi

6. Automated test scripts for CI pipelines

In a CI pipeline, a clear exit code matters most: 0 for success, non-zero for failure. A script that tests WebSocket connections with Bash should therefore wrap the handshake, message exchange, and clean connection teardown in a function and abort immediately with a meaningful message on any failure. set -euo pipefail is just as mandatory here as in any other production Bash script.

For GitLab CI or GitHub Actions, a job that downloads websocat as a static binary from cache or via curl, points the test endpoint at the feature branch, and runs the test script is enough. Retries with a limited count absorb flaky network connections in the CI environment without hiding genuine failures.

Task Fragile / Cumbersome Recommended Pattern Benefit
Verify handshake Manual browser DevTools openssl s_client + upgrade headers Automatable, every byte visible
Send/receive message Custom handshake code websocat -n1 No frame handling needed
Long-term stability Manually holding a terminal open websocat --ping-interval Uncovers proxy idle timeouts
CI integration No exit code, result only visual set -euo pipefail + timeout Clear pass/fail status in the pipeline
Diagnose TLS errors websocat error message alone openssl s_client -showcerts Shows certificate chain and handshake detail

7. Diagnosing timeouts, proxies and TLS problems

The most common source of trouble in testing is not the WebSocket server itself but a reverse proxy in between that drops the upgrade header. Nginx, for instance, requires explicit configuration with proxy_set_header Upgrade $http_upgrade; and proxy_set_header Connection "upgrade";, otherwise the WebSocket handshake becomes a normal HTTP request that fails with status 400 or 426. Anyone who wants to test WebSocket connections with Bash and immediately recognize a failure at the proxy level should check the response status code first before digging deeper into the stack.

TLS problems show up differently: openssl s_client -connect host:443 -showcerts shows the full certificate chain and immediately reveals whether an intermediate certificate is missing or the certificate has already expired. Timeout problems, in turn, usually occur with load balancers that drop idle connections after a fixed time without sending a close frame, the script then hangs in a read with no response, which is why an explicit timeout wrapper around every test call is mandatory.

A third, often overlooked case: some APIs require additional headers, such as an authentication token, already in the upgrade request, not only after the handshake. If this header is missing, the server often responds with a generic 401, which is easily mistaken for a fundamental handshake problem.

8. Logging and monitoring WebSocket sessions

A single test is not enough for production monitoring. A monitoring script that regularly tests WebSocket connections with Bash should log a timestamp, the handshake response time, and the time until the first payload response. These values can be written to a simple CSV file or handed directly to a monitoring system like Prometheus via the Node Exporter textfile collector.

A clear separation between handshake errors, timeout errors, and protocol errors in the log lines is important, because these three categories have different root causes and different responsible teams: networking, application, or infrastructure. A cron job that runs every five minutes and only triggers a notification after three consecutive failures prevents false alarms from single brief network fluctuations while still reliably reporting genuine outages.

Mironsoft

Shell automation, API testing and deployment infrastructure

Real-time interfaces you can rely on being tested?

We build Bash-based test suites for WebSocket endpoints, wire them into your CI pipeline, and set up monitoring for real-time connections in Magento and Node environments.

Test Scripts

websocat and openssl based test suites for handshake and payload

CI Integration

WebSocket checks as a pipeline gate before every deployment

Monitoring

Regular ping/pong checks with alerting on outages

9. WebSocket connection tools compared

The table in section six already shows the central recommendations, but it is worth distinguishing the tools by where they are used as well. On a production server with no additional software, openssl s_client is often the only option to test WebSocket connections with Bash. In CI environments where you control the image, websocat is the far more convenient choice because it fully handles the handshake and framing.

curl with experimental WebSocket support is mainly suitable when a team already relies heavily on curl for all other API tests and consistency in tooling matters more than full feature coverage. For more complex test scenarios with several simultaneous connections or load tests, websocat remains the most robust base, because it can be embedded in simple Bash loops with background processes.

10. Summary

Testing WebSocket connections with Bash means checking the handshake, the message exchange, and connection stability individually and automatically. openssl s_client is suitable for manually diagnosing handshake and TLS problems, while websocat is the most practical choice for automated tests and CI integration. Ping/pong control frames show whether a connection stays stable over a longer period, which is critical especially behind load balancers and reverse proxies.

The biggest win comes when teams integrate WebSocket tests into CI just as naturally as classic HTTP tests. A script with set -euo pipefail, clear exit codes, and a clean separation between handshake, timeout, and protocol errors delivers results a team can rely on without checking manually in the browser every time.

Testing WebSocket Connections with Bash — The Essentials at a Glance

Handshake

Send the upgrade header plus Sec-WebSocket-Key via openssl s_client, check the response for status 101.

Testing Tool

websocat -n1 handles handshake and framing, ideal for CI without extra dependencies.

Stability

--ping-interval reliably uncovers idle timeouts from proxies and load balancers.

CI and Monitoring

Clear exit codes, a timeout wrapper, and separate error categories for handshake, timeout, and protocol.

11. FAQ: Testing WebSocket Connections with Bash

1Possible with no extra tools?
The handshake yes, via openssl s_client and printf. The frame format including masking is tedious without a tool like websocat.
2What is websocat?
A static binary that treats WebSocket like a pipe: send stdin, receive stdout. Ideal for Bash pipelines.
3Proxy blocks the upgrade, how to tell?
Status 400 or 426 instead of 101. Check response headers with openssl s_client to see whether the upgrade header even arrives.
4Why ping/pong for testing?
Shows stability over time. Load balancers often drop inactive connections, which only a long-running test reveals.
5Integration into a CI pipeline?
A dedicated job with websocat as a binary, timeout, and set -euo pipefail for a clear exit code.
6TLS error vs. application error?
openssl s_client -showcerts shows the certificate chain. If TLS fails first, that's the cause. If the upgrade fails afterward, it's the server.
7Test multiple connections in parallel?
Yes, via background processes with & and a PID array, each websocat instance runs independently.
8curl upgrade vs. websocat?
curl tests the handshake. websocat also manages the frame format for complete payload tests.
9Authentication during the handshake?
As an additional header (Authorization: Bearer TOKEN) or a query parameter. websocat supports --header for this.
10Log test results for monitoring?
Timestamp and error category into CSV or Prometheus textfile. Wait for several consecutive failures before alerting.