The HTTP/1.1 upgrade shown in the cover is the opening flow discussed here; other HTTP versions use different bootstrapping. HTTP describes requests and responses; it does not require a new TCP connection for every request. WebSocket adds a message-oriented, bidirectional session. Choosing it over polling means accepting responsibility for that session’s identity, queues, liveness and recovery, in exchange for timely updates initiated by either endpoint.

A client authenticates and upgrades; the server returns 101. Messages then flow both ways and heartbeat ping/pong checks liveness. A missed heartbeat deadline leads to close and cleanup, exponential backoff with jitter, fresh authentication, and rebuilding subscriptions in a new session.
HTTP/1.1 example. Heartbeat deadlines and authentication are application policy; reconnecting does not automatically replay missed messages.

What an upgrade buys

This article’s exercise uses HTTP/1.1. An accepted opening handshake returns 101, after which WebSocket frames travel over the connection. Validate whole, case-insensitive Upgrade and Connection list members, version 13, and a Base64 key decoding to sixteen bytes. A substring such as notupgrade is insufficient. The accept value confirms the handshake; it is not authentication. RFC 6455, sections 4.2 and 10.5.

The 101 response has no HTTP message body. It must omit Content-Length, even zero, and Transfer-Encoding. Subsequent bytes are WebSocket frames, not an empty HTTP response body followed by arbitrary text. RFC 9110, section 8.6 and RFC 9112, section 6.1.

HTTP/2 has a different opening procedure. RFC 8441 uses extended CONNECT on one stream, after the server advertises support. It uses :protocol websocket and a successful 2xx response, without the HTTP/1.1 Upgrade/Connection fields or key/accept exchange. Other streams can share the connection. This fixture does not test that mechanism. RFC 8441, sections 3–5.

Polling is still a valid baseline

Polling is a useful baseline when updates are infrequent and some delay is acceptable. It offers familiar request boundaries and can benefit from caching when responses permit it. Server-Sent Events can fit predominantly server-to-client updates.

In an application design, authenticate before granting access, check browser origins, authorize subscriptions and define what happens when credentials expire or permissions change. A reconnect should establish fresh authorization and rebuild subscriptions. Applications may carry authorization information within messages, but WebSocket does not invent that policy. Likewise, reconnecting alone neither restores subscriptions nor replays missed events: use application sequence numbers, resume cursors or a fresh snapshot where correctness requires them.

Keep the session healthy

Bound each connection’s outgoing queue and watch slow consumers. Depending on the data, coalesce superseded updates, reduce delivery frequency or disconnect a lagging subscriber. Apply corresponding limits on the receiving side. Otherwise, a live peer can still exhaust memory or overwhelm a user interface. Backpressure is a capacity problem; a successful heartbeat does not show that application messages are being processed quickly.

Protocol Ping/Pong uses control frames, not raw ping strings. Browser JavaScript does not directly expose those control frames through the WebSocket API; an application-level heartbeat is a separately defined message exchange. Clients mask frames; servers do not. Control frames are unfragmented and at most 125 payload bytes. A replying Pong echoes the Ping payload. Track one outstanding Ping and its deadline; unrelated or unsolicited Pongs need not satisfy that application check. RFC 6455, sections 5.1 and 5.5.

When it breaks

A quiet connection may encounter an intermediary’s idle timeout, but there is no universal sixty-second cutoff or mandated two-missed-Pong rule. Choose heartbeat timing from the actual path and service objectives. Silence without an outstanding Ping proves no missed response. Even an expired deadline shows that the response was not observed in time, rather than proving which machine or network component failed.

After a restart, immediate synchronized reconnects can overload authentication and subscription handling. Use bounded exponential backoff with jitter, release the old session’s resources and make recovery repeatable. Test these deployment behaviors separately; the small exercise below cannot establish production resilience.

Check it quickly

Save the complete program as ws_session_probe.py. The socket-free command is python3 ws_session_probe.py –offline. It checks parsers and state only. In an explicitly authorized isolated local environment, python3 ws_session_probe.py –loopback creates its own random 127.0.0.1 port and runs sixteen finite cases, with no dependencies or external connections.

"""Finite HTTP/1.1 WebSocket control-frame fixture, not a production server.
No sockets are created on import or with --offline. --loopback is explicit.
"""
import argparse
import base64
import binascii
import hashlib
import io
import os
import queue
import re
import socket
import threading
import time

GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
VECTOR_KEY = "dGhlIHNhbXBsZSBub25jZQ=="
VECTOR_ACCEPT = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="
HOST = "127.0.0.1"
IO_SECONDS = 3.0
PONG_SECONDS = 0.75  # Exercise policy, not an RFC or deployment default.
MAX_HEADER = 8192
MAX_FIELDS = 32
TOKEN = r"[!#$%&'*+.^_`|~0-9A-Za-z-]+"


def check(ok, message):
    if not ok:
        raise AssertionError(message)  # Also active under python -O.


def accept_value(key):
    return base64.b64encode(hashlib.sha1((key + GUID).encode("ascii")).digest()).decode()


def parse_head(raw):
    if len(raw) > MAX_HEADER or not raw.endswith(b"\r\n\r\n"):
        raise ValueError("header limit or termination")
    lines = raw[:-4].decode("ascii").split("\r\n")
    if len(lines) - 1 > MAX_FIELDS:
        raise ValueError("field limit")
    headers = {}
    for line in lines[1:]:
        name, colon, value = line.partition(":")
        if not colon or not re.fullmatch(TOKEN, name):
            raise ValueError("field syntax")
        value = value.strip(" \t")
        if any(ord(c) < 32 and c != "\t" or ord(c) == 127 for c in value):
            raise ValueError("field control byte")
        name = name.lower()
        if name in headers:
            if name not in ("connection", "upgrade"):
                raise ValueError("duplicate singleton (fixture policy)")
            value = headers[name] + "," + value
        headers[name] = value
    return lines[0], headers


def list_values(value, protocol=False):
    grammar = TOKEN + (r"(?:/" + TOKEN + r")?" if protocol else "")
    parts = [part.strip(" \t") for part in value.split(",") if part.strip(" \t")]
    if not parts or any(not re.fullmatch(grammar, part) for part in parts):
        raise ValueError("invalid token list")
    return {part.lower() for part in parts}


def validate_request(raw, authority):
    try:
        start, h = parse_head(raw)
        if start != "GET /chat HTTP/1.1" or h.get("host") != authority:
            return 400, None
        if "websocket" not in list_values(h.get("upgrade", ""), protocol=True):
            return 400, None
        if "upgrade" not in list_values(h.get("connection", "")):
            return 400, None
        # The fixture has no request-body support and never negotiates extensions.
        if "content-length" in h or "transfer-encoding" in h:
            return 400, None
        key = h.get("sec-websocket-key", "")
        if len(base64.b64decode(key, validate=True)) != 16:
            return 400, None
        version = h.get("sec-websocket-version", "")
        if not re.fullmatch(r"0|[1-9][0-9]{0,2}", version) or int(version) > 255:
            return 400, None
        if version != "13":
            return 426, None
        return 101, key
    except (ValueError, UnicodeError, binascii.Error):
        return 400, None


def response(status, key=None):
    if status == 101:
        return ("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n"
                "Connection: Upgrade\r\nSec-WebSocket-Accept: " + accept_value(key)
                + "\r\n\r\n").encode("ascii")
    # An empty ordinary HTTP error response may use Content-Length.
    extra = "Sec-WebSocket-Version: 13\r\nUpgrade: websocket\r\n" if status == 426 else ""
    reason = "Upgrade Required" if status == 426 else "Bad Request"
    connection = "Upgrade, close" if status == 426 else "close"
    return (f"HTTP/1.1 {status} {reason}\r\n{extra}"
            f"Connection: {connection}\r\nContent-Length: 0\r\n\r\n").encode("ascii")


def exact(sock, n, deadline):
    data = bytearray()
    while len(data) < n:
        remaining = deadline - time.monotonic()
        if remaining <= 0:
            raise TimeoutError("absolute receive deadline")
        sock.settimeout(remaining)
        chunk = sock.recv(n - len(data))
        if not chunk:
            raise EOFError("peer closed early")
        data.extend(chunk)
    return bytes(data)


def receive_head(sock):
    deadline = time.monotonic() + IO_SECONDS
    data = bytearray()
    while not data.endswith(b"\r\n\r\n"):
        if len(data) >= MAX_HEADER:
            raise ValueError("header limit")
        data.extend(exact(sock, 1, deadline))  # Never consumes following frames.
    return bytes(data)


def send(sock, data):
    sock.settimeout(IO_SECONDS)
    sock.sendall(data)


def frame(opcode, payload, masked=False):
    if opcode not in (8, 9, 10) or len(payload) > 125:
        raise ValueError("fixture supports short control frames only")
    mask = os.urandom(4) if masked else b""
    body = bytes(b ^ mask[i % 4] for i, b in enumerate(payload)) if masked else payload
    return bytes((0x80 | opcode, (0x80 if masked else 0) | len(payload))) + mask + body


def parse_control(read, expect_masked):
    """read(n) must return exactly n bytes or raise, as exact() does."""
    first, second = read(2)
    opcode, size = first & 15, second & 127
    if first & 0x70 or not first & 0x80 or opcode not in (8, 9, 10) or size > 125:
        raise ValueError("invalid or unsupported control frame")
    if bool(second & 0x80) != expect_masked:
        raise ValueError("wrong mask direction")
    mask = read(4) if expect_masked else b""
    data = read(size)
    payload = bytes(b ^ mask[i % 4] for i, b in enumerate(data)) if mask else data
    # Only our empty/1000/1002/1008 Close bodies are within this fixture's scope.
    if opcode == 8 and payload not in (b"", b"\x03\xe8", b"\x03\xea", b"\x03\xf0"):
        raise ValueError("Close body outside fixture scope")
    return opcode, payload


def receive_control(sock, masked, deadline=None):
    end = time.monotonic() + IO_SECONDS if deadline is None else deadline
    return parse_control(lambda n: exact(sock, n, end), masked)


class Heartbeat:
    def __init__(self):
        self.outstandingPing = None
        self.deadline = None

    def sent(self, payload, now):
        check(self.outstandingPing is None, "only one outstanding Ping")
        self.outstandingPing, self.deadline = payload, now + PONG_SECONDS

    def pong(self, payload, now):
        if self.outstandingPing is not None and payload == self.outstandingPing and now < self.deadline:
            self.outstandingPing, self.deadline = None, None
            return True
        return False

    def expired(self, now):
        return self.outstandingPing is not None and now >= self.deadline


def server_session(conn):
    try:
        opcode, payload = receive_control(conn, True)
    except ValueError:
        send(conn, frame(8, b"\x03\xea"))  # 1002 protocol error.
        return "invalid-frame-refused"
    check((opcode, payload) == (9, b"client-ping"), "masked client Ping")
    send(conn, frame(10, payload))
    hb = Heartbeat()
    nonce = os.urandom(8)
    send(conn, frame(9, nonce))
    hb.sent(nonce, time.monotonic())  # Record only after successful Ping send.
    mismatches = 0
    for _ in range(4):  # A malicious/unexpected frame stream cannot loop forever.
        try:
            opcode, payload = receive_control(conn, True, hb.deadline)
        except TimeoutError:
            check(hb.expired(time.monotonic()), "timeout needs outstanding Ping")
            send(conn, frame(8, b"\x03\xf0"))  # 1008: this exercise's policy.
            check(receive_control(conn, True) == (8, b"\x03\xf0"), "Close reply")
            return "missing-pong-expired"
        check(opcode == 10, "expected Pong in bounded heartbeat scenario")
        if hb.pong(payload, time.monotonic()):
            check(mismatches == 1 and hb.outstandingPing is None, "matched Pong clears state")
            send(conn, frame(8, b"\x03\xe8"))
            check(receive_control(conn, True) == (8, b"\x03\xe8"), "normal Close reply")
            return "mismatch-ignored-match-cleared"
        mismatches += 1
    raise AssertionError("heartbeat frame budget exhausted")


def request(authority, changes=None):
    h = {"Host": authority, "Upgrade": "WebSocket", "Connection": "keep-alive, UpGrAdE",
         "Sec-WebSocket-Key": base64.b64encode(os.urandom(16)).decode(), "Sec-WebSocket-Version": "13"}
    for key, value in (changes or {}).items():
        if value is None:
            h.pop(key, None)
        else:
            h[key] = value
    return ("GET /chat HTTP/1.1\r\n" + "".join(f"{k}: {v}\r\n" for k, v in h.items())
            + "\r\n").encode("ascii")


BAD_REQUESTS = [
    ("connection-substring", {"Connection": "notupgrade"}, 400),
    ("connection-syntax", {"Connection": "Upgrade;bad"}, 400),
    ("upgrade-substring", {"Upgrade": "xwebsocket"}, 400),
    ("upgrade-syntax", {"Upgrade": "websocket;bad"}, 400),
    ("missing-version", {"Sec-WebSocket-Version": None}, 400),
    ("malformed-version", {"Sec-WebSocket-Version": "13,12"}, 400),
    ("unsupported-version", {"Sec-WebSocket-Version": "12"}, 426),
    ("missing-key", {"Sec-WebSocket-Key": None}, 400),
    ("malformed-key", {"Sec-WebSocket-Key": "!not-base64!"}, 400),
    ("short-key", {"Sec-WebSocket-Key": "c2hvcnQ="}, 400),
]

# Invalid two-byte headers are rejected before any body is required/read.
BAD_FRAMES = [
    ("unmasked-client", b"\x89\x00"),
    ("fragmented-control", b"\x09\x80"),
    ("oversized-control", b"\x89\xfe"),
    ("reserved-bit", b"\xc9\x80"),
]


def offline():
    check(accept_value(VECTOR_KEY) == VECTOR_ACCEPT, "RFC accept vector")
    for name, changes, expected in BAD_REQUESTS:
        check(validate_request(request("127.0.0.1:1", changes), "127.0.0.1:1")[0] == expected, name)
    check(validate_request(request("127.0.0.1:1"), "127.0.0.1:1")[0] == 101, "valid tokens")
    for masked in (False, True):
        reader = io.BytesIO(frame(9, b"hello", masked)).read
        check(parse_control(reader, masked) == (9, b"hello"), "frame round trip")
    for name, raw in BAD_FRAMES:
        try:
            parse_control(io.BytesIO(raw).read, True)
        except ValueError:
            pass
        else:
            raise AssertionError(name)
    hb = Heartbeat()
    check(not hb.expired(100), "idle has no missed-Pong deadline")
    hb.sent(b"p", 100)
    check(not hb.pong(b"wrong", 100.1), "mismatched Pong must not clear")
    check(hb.pong(b"p", 100.2) and not hb.expired(200), "matched Pong clears")
    hb.sent(b"q", 200)
    check(not hb.expired(200.1) and hb.expired(201), "outstanding deadline")
    check(not hb.pong(b"q", 201), "late Pong does not revive expired state")
    start, h = parse_head(response(101, VECTOR_KEY))
    check(start == "HTTP/1.1 101 Switching Protocols", "exact status")
    check("content-length" not in h and "transfer-encoding" not in h, "no 101 HTTP body framing")
    print("OFFLINE ONLY: parser, vector, frame codec and state checks passed; sockets unexecuted")


def loopback():
    cases = [(name, changes, status, None) for name, changes, status in BAD_REQUESTS]
    cases += [(name, {}, 101, raw) for name, raw in BAD_FRAMES]
    cases += [("matched-pong", {}, 101, None), ("missing-pong", {}, 101, None)]
    results = queue.Queue()
    stop = threading.Event()
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as listener:
        listener.bind((HOST, 0))  # OS-selected port; never a fixed or external service.
        listener.listen(1)
        listener.settimeout(IO_SECONDS)
        authority = f"{HOST}:{listener.getsockname()[1]}"
        print(f"OWNED_LOOPBACK {authority}", flush=True)

        def worker():
            try:
                for _ in cases:
                    if stop.is_set():
                        return
                    conn, peer = listener.accept()
                    with conn:
                        check(peer[0] == HOST, "loopback peer")
                        status, key = validate_request(receive_head(conn), authority)
                        send(conn, response(status, key))
                        results.put(server_session(conn) if status == 101 else f"http-{status}")
            except Exception as error:
                results.put(error)

        thread = threading.Thread(target=worker, name="owned-ws-fixture", daemon=False)
        thread.start()
        try:
            for name, changes, expected, bad_frame in cases:
                with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client:
                    client.settimeout(IO_SECONDS)
                    client.connect(listener.getsockname())  # No DNS or external request.
                    raw = request(authority, changes)
                    send(client, raw)
                    start, h = parse_head(receive_head(client))
                    check(start.split(" ")[1] == str(expected), name + " status")
                    if expected != 101:
                        check("sec-websocket-accept" not in h, name + " no upgrade")
                        if expected == 426:
                            check(h.get("sec-websocket-version") == "13", "advertise supported version")
                        wanted = f"http-{expected}"
                    else:
                        key = parse_head(raw)[1]["sec-websocket-key"]
                        check(h.get("sec-websocket-accept") == accept_value(key), "accept matches nonce")
                        check(list_values(h.get("upgrade", ""), True) == {"websocket"}, "response upgrade")
                        check(list_values(h.get("connection", "")) == {"upgrade"}, "response connection")
                        check("content-length" not in h and "transfer-encoding" not in h, "101 framing")
                        if bad_frame is not None:
                            send(client, bad_frame)
                            check(receive_control(client, False) == (8, b"\x03\xea"), "1002 rejection")
                            wanted = "invalid-frame-refused"
                        else:
                            send(client, frame(9, b"client-ping", True))
                            check(receive_control(client, False) == (10, b"client-ping"), "echoed Pong")
                            opcode, nonce = receive_control(client, False)
                            check(opcode == 9 and len(nonce) == 8, "server Ping received")
                            if name == "matched-pong":
                                send(client, frame(10, b"wrong", True))
                                send(client, frame(10, nonce, True))
                                close = b"\x03\xe8"
                                wanted = "mismatch-ignored-match-cleared"
                            else:
                                # Keep the TCP socket open but deliberately send no Pong.
                                close = b"\x03\xf0"
                                wanted = "missing-pong-expired"
                            check(receive_control(client, False) == (8, close), "server Close")
                            send(client, frame(8, close, True))
                    outcome = results.get(timeout=2 * IO_SECONDS)
                    if isinstance(outcome, Exception):
                        raise outcome
                    check(outcome == wanted, name + " server outcome")
                    client.settimeout(IO_SECONDS)
                    check(client.recv(1) == b"", name + " owned connection closed")
                    print("CASE_OK", name, outcome, flush=True)
        finally:
            stop.set()
            # Every blocking operation is bounded; failure closes the active client.
            thread.join(timeout=3 * IO_SECONDS + PONG_SECONDS)
            check(not thread.is_alive(), "owned worker failed to stop")
    check(listener.fileno() == -1, "owned listener closed")
    print(f"PASS LOOPBACK: {len(cases)} cases; owned sockets closed; worker joined")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=__doc__)
    mode = parser.add_mutually_exclusive_group(required=True)
    mode.add_argument("--offline", action="store_true")
    mode.add_argument("--loopback", action="store_true")
    args = parser.parse_args()
    offline() if args.offline else loopback()

Expected live results cover ten refused handshakes, four invalid control frames, a mismatched then matching Pong, and a deliberately missing Pong after a real Ping. Successful execution ends with PASS LOOPBACK and confirms owned sockets are closed and the worker joined; any failed check raises. Headers, frames and I/O have explicit bounds.

Historical execution: this exact shared program completed one supervised sixteen-case trial on 2026-10-17 with Python 3.14.4 on macOS 26.5.1 arm64. Both language listings contain those same program bytes; this was one shared trial, not two independent language runs. Its retained output and cleanup record support only the cases described here. No socket or browser execution was repeated for this source review. This is a teaching fixture with a narrow HTTP parser and limited control frames, not a complete WebSocket server. TLS, browsers, HTTP/2, data fragmentation, compression, proxy behavior, load and production authentication remain outside its coverage.

Further reading

Sources