WebSocket connection failed while sending 10KB binary data

Hello, I'm creating a WebSocket connection to my server from the safari browser via console tab like below.

let socket = new WebSocket('wss://localhost:1200/WS_TEST?client=123&session=1234'); socket.onopen = function (event) { console.log('WebSocket connection opened:', event); };

socket.onmessage = function (event) { console.log('Received buffer', event.data); };

Once the connection established, The server sends multiple data which includes text and binary data.

When the server sends binary data more than 10KB after sending text data in the same socket, the WebSocket connection is getting failed.

EX: Frame Type(Text): "Hi" Frame Type(Binary): just binary frame of size 11KB

When the server send the binary data alone, the connection is not affected even the size is more than 100KBs

EX: Frame Type(Binary): just binary frame of size 100KB

I can confirm a bug like this in Safari 17.6 (19618.3.11.11.5).

It seems to happen when permessage-deflate has been negotiated, and the server sends a TEXT message followed by a BINARY message, and the BINARY message contains specific content and spans multiple frames.

The bug seems to occur when the first frame is at least 2,048 bytes and the original data fed to the compressor is "binary-ish". I was unable to reproduce the bug by compressing only alphanumeric characters. Maybe a compression ratio thing.

The following python code implements a server to demonstrate the bug. Connecting with Safari (e.g. by running var ws = new WebSocket('http://localhost:3000'); in the developer console) yields "The operation couldn’t be completed. Protocol error", whereas it works fine in Chrome.

from base64 import b64encode
import hashlib
import socket
import time
import zlib

PORT = 3000
WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"

RAND_GENERATOR = None


def rand_byte():
    global RAND_GENERATOR

    if not RAND_GENERATOR:
        # from https://en.wikipedia.org/wiki/Linear_congruential_generator
        def lcg(modulus, a, c, seed):
            """Linear congruential generator."""
            while True:
                seed = (a * seed + c) % modulus
                yield seed

        # glibc-style config
        RAND_GENERATOR = lcg(2**31, 1103515245, 12345, 0)

    # take most significant 8 bits
    return next(RAND_GENERATOR) >> 23


def handle_conn(conn, msgs):
    print("Connected by", addr)

    req = b""
    while b"\r\n\r\n" not in req:
        req += conn.recv(1024)

    key = ""
    ext = ""
    for line in req.decode("utf-8").split("\r\n")[1:]:
        if not line:
            continue
        name, value = line.split(":", 1)
        if name == "Sec-WebSocket-Key":
            key = value.strip()
        if name == "Sec-WebSocket-Extensions":
            ext = value.strip()

    print(f"ext: {ext}")
    if "permessage-deflate" not in ext:
        raise ValueError("client did not offer permessage-deflate")

    accept = b64encode(hashlib.sha1((key + WS_GUID).encode("utf-8")).digest()).decode(
        "utf-8"
    )

    conn.sendall(
        (
            "HTTP/1.1 101 Switching Protocols\r\n"
            "Upgrade: websocket\r\n"
            "Connection: Upgrade\r\n"
            f"Sec-WebSocket-Accept: {accept}\r\n"
            "Sec-WebSocket-Extensions: permessage-deflate\r\n"
            "\r\n"
        ).encode("utf-8")
    )

    # fin=true rsv1=true opcode=text
    header = bytes([0xC1, len(encoded_msgs[0])])
    conn.send(header + encoded_msgs[0])

    # fin=false rsv1=true opcode=binary
    header = bytes([0x42, 0x7E, 0x08, 0x00])
    conn.send(header + encoded_msgs[1][:2048])

    # fin=true rsv1=false opcode=continuation
    header = bytes([0x80, len(encoded_msgs[1]) - 2048])
    conn.send(header + encoded_msgs[1][2048:])

    # fin=true rsv1=true opcode=text
    header = bytes([0xC1, len(encoded_msgs[2])])
    conn.send(header + encoded_msgs[2])

    time.sleep(1)


plain_msgs = [
    b"hello",
    bytes([rand_byte() for _ in range(2100)]),
    b"world",
]

encoder = zlib.compressobj(wbits=-15)

encoded_msgs = []

for msg in plain_msgs:
    msg_enc = encoder.compress(msg) + encoder.flush(zlib.Z_SYNC_FLUSH)
    assert msg_enc[-4:] == b"\x00\x00\xff\xff"
    msg_enc = msg_enc[:-4]
    encoded_msgs.append(msg_enc)

assert len(encoded_msgs[0]) < 126
assert len(encoded_msgs[1]) >= 2048 and len(encoded_msgs[1]) < 2048 + 126
assert len(encoded_msgs[2]) < 126

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind(("", PORT))
    s.listen(1)
    print(f"Listening on port {PORT}")
    while True:
        conn, addr = s.accept()
        with conn:
            handle_conn(conn, encoded_msgs)
WebSocket connection failed while sending 10KB binary data
 
 
Q