这是indexloc提供的服务,不要输入任何密码
Skip to content

Conversation

@guest271314
Copy link
Contributor

Interoperability is all over the place.

For WebSocket clients

  • Chromium 138 Developer Build (Linux)

{ conn: { [Symbol(kHandle)]: {} } }
{ opcode: 8 }
Close opcode.
TypeError: Parameter 1 is required in 'respond'.
at $ (polyfills.js:28:14849)
at respond (polyfills.js:28:21853)
at pull (core.js{ conn: { [Symbol(kHandle)]: {} } }
{ opcode: 8 }
Close opcode.
WebSocket client connection closed
Error: EPIPE: broken pipe
:1:11961)

  • Firefox Nightly 140.0a1 (2025-05-07) (64-bit)

Works with 127.0.0.1:8080

Does not work with 0.0.0.0:8080

  • Bun 1.2.13

Works with builtin or Undici. Always causes broken pipe in tjs.

{ conn: { [Symbol(kHandle)]: {} } }
{ opcode: 8 }
Close opcode.
WebSocket client connection closed
Error: EPIPE: broken pipe

  • Deno 2.3.1+d372c0d (canary, release, x86_64-unknown-linux-gnu)

Does nothing. Using builtin or Undici. 0.0.0.0, 127.0.0.1, or localhost.

  • Node.js v24.0.0-nightly202505066102159fa1

Does nothing. Using builtin or Undici. 0.0.0.0, 127.0.0.1, or localhost.

For WebSocketStream clients

  • Chromium 138 Developer Build (Linux)

Works.

writer.close() does not cause broken pipe. A different error. Server does not exit.

TypeError: Parameter 1 is required in 'respond'.
at $ (polyfills.js:28:14849)
at respond (polyfills.js:28:21853)
at pull (core.js:1:11961)

  • Deno

Does nothing. Using builtin or Undici. 0.0.0.0, 127.0.0.1, or localhost.

  • Node.js

Does nothing. Using builtin or Undici. 0.0.0.0, 127.0.0.1, or localhost.

websocket-client.js. Echo 1 MB.

// import { WebSocket } from "undici";

var ws = new WebSocket("ws://127.0.0.1:8080");
ws.binaryType = "arraybuffer";
ws.addEventListener("open", (e) => {
  console.log(e);
  write();
});
ws.addEventListener("close", (e) => {
  console.log(e);
});
ws.addEventListener("message", (e) => {
  const v = e.data;
  if (typeof v === "string") {
    console.log(v);
  } else {
    const decoded = decoder.decode(v, {
      stream: true,
    });
    console.log(len += v.byteLength, [...decoded].every((s) => s === "a"));
  }
  if (len === data.buffer.byteLength) {
    console.log(ws.bufferedAmount);
    ws.close();
  }
});
ws.addEventListener("error", (e) => {
  console.log(e);
});

var len = 0;
var encoder = new TextEncoder();
var decoder = new TextDecoder();
var data = new Uint8Array(1024 ** 2).fill(97);
var len = 0;

function write() {
  for (let i = 0; i < data.length; i += 65536) {
    try {
      console.log(ws.bufferedAmount);
      ws.send(data.subarray(i, i + 65536));
    } catch (e) {
      console.warn(e);
    }
  }
}

websocket-stream-client.js. Echo 7 MB.

// Only aborts *before* the handshake
import { WebSocketStream } from "undici";
// Only aborts *before* the handshake
var abortable = new AbortController();
var {
  signal,
} = abortable;
var wss = new WebSocketStream("ws://0.0.0.0:8080", {
  signal
});
console.log(wss);

var {
  readable,
  writable,
} = await wss.opened.catch(console.warn);
wss.closed.then(() => console.log("WebSocketStream closed.")).catch((e) => {
  console.log(e);
});
console.log(readable);
var writer = writable.getWriter();
var reader = readable.getReader();
var len = 0;
var encoder = new TextEncoder();
var decoder = new TextDecoder();
var data = new Uint8Array(1024 ** 2 * 7).fill(97);
var len = 0;
for (let i = 0; i < data.length; i += 65536) {
  try {
    await writer.ready;
    writer.write(data.subarray(i, i + 65536));
    // console.log(writer.desiredSize);
    const {
      value: v,
      done,
    } = await reader.read();
    if (typeof v === "string") {
      console.log(v);
    } else {
      const decoded = decoder.decode(v, {
        stream: true,
      });
      console.log(
        len += v.byteLength,
        v,
        [...decoded].every((s) => s === "a"),
      );
    }
  } catch (e) {
    console.warn(e);
  }
}
console.assert(len === data.buffer.byteLength, [len, data.buffer.byteLength]);
console.log(len, data.buffer.byteLength);
await writer.write("Text").then(() => reader.read()).then(console.log).catch(
  console.warn,
);
await writer.close();

Usage in tjs runtime tjs run tjs-websocket-server.js

import { WebSocketConnection } from "./websocket-server.js";

const decoder = new TextDecoder();

const listener = await tjs.listen("tcp", "0.0.0.0", "8080");
const { family, ip, port } = listener.localAddress;
console.log(
  `${navigator.userAgent} WebSocket server listening on family: ${family}, ip: ${ip}, port: ${port}`,
);

// TODO: Don't exit loop when broken pipe happens (Bun 1.2.13)
while (true) {
  try {
    const conn = await listener.accept();
    console.log({ conn });
    const writer = conn.writable.getWriter();
    const { readable: wsReadable, writable: wsWritable } = new TransformStream(),
      wsWriter = wsWritable.getWriter();
    let ws;
    for await (const value of conn.readable) {
      const request = decoder.decode(value);
      if (request.includes("Upgrade: websocket")) {
        const [key] = request.match(/(?<=Sec-WebSocket-Key: ).+/i);
        const handshake = await WebSocketConnection.hashWebSocketKey(
          key,
          writer,
        );
        ws = new WebSocketConnection(wsReadable, writer)
          .processWebSocketStream().catch((e) => {
            throw e;
          });
      } else {
        await wsWriter.ready;
        await wsWriter.write(new Uint8Array(value));
      }
    }

    console.log("WebSocket client connection closed");
    // await wsWriter.close();
  } catch (e) {
    // listener.close();
    console.log(e);
/*

{ conn: { [Symbol(kHandle)]: {} } }
{ opcode: 8 }
Close opcode.
WebSocket client connection closed
txiki.js/24.12.0 WebSocket server listening on family: 4, ip: 0.0.0.0, port: 8080
Error: EPIPE: broken pipe

Or

{ conn: { [Symbol(kHandle)]: {} } }
{ opcode: 8 }
Close opcode.
TypeError: Parameter 1 is required in 'respond'.
    at $ (polyfills.js:28:14849)
    at respond (polyfills.js:28:21853)
    at pull (core.js:1:11961)

Unpredictable which will happen, or not
*/
  } finally {
    continue;
  }
}

Interoperability is all over the place. 

For WebSocket clients

- Chromium 138 Developer Build (Linux)

{ conn: { [Symbol(kHandle)]: {} } }
{ opcode: 8 }
Close opcode.
TypeError: Parameter 1 is required in 'respond'.
    at $ (polyfills.js:28:14849)
    at respond (polyfills.js:28:21853)
    at pull (core.js{ conn: { [Symbol(kHandle)]: {} } }
{ opcode: 8 }
Close opcode.
WebSocket client connection closed
Error: EPIPE: broken pipe
:1:11961)


- Firefox Nightly 140.0a1 (2025-05-07) (64-bit)

Works with 127.0.0.1:8080

Does not work with 0.0.0.0:8080

- Bun 1.2.13

Works with builtin or Undici. Always causes broken pipe in tjs.

{ conn: { [Symbol(kHandle)]: {} } }
{ opcode: 8 }
Close opcode.
WebSocket client connection closed
Error: EPIPE: broken pipe

- Deno 2.3.1+d372c0d (canary, release, x86_64-unknown-linux-gnu)

Does nothing. Using builtin or Undici. 0.0.0.0, 127.0.0.1, or localhost.

- Node.js v24.0.0-nightly202505066102159fa1

Does nothing. Using builtin or Undici. 0.0.0.0, 127.0.0.1, or localhost.

For WebSocketStream clients

- Chromium 138 Developer Build (Linux)

Works.

writer.close() does not cause broken pipe. A different error. 
Server does not exit. 

TypeError: Parameter 1 is required in 'respond'.
    at $ (polyfills.js:28:14849)
    at respond (polyfills.js:28:21853)
    at pull (core.js:1:11961)

- Deno 

Does nothing. Using builtin or Undici. 0.0.0.0, 127.0.0.1, or localhost.

- Node.js 

Does nothing. Using builtin or Undici. 0.0.0.0, 127.0.0.1, or localhost.
@guest271314
Copy link
Contributor Author

More all over the place with regard to clients. When I adjust the code to use a single resizable ArrayBuffer to temporarily store incoming data to the server Chromium 138 works as expected. Firefox 140, and Bun, and Node.js, and Deno don't.

This

    const data = buf.subarray(idx + length);
    this.buffer.resize(data.length);
    for (let i = 0; i < this.buffer.byteLength; i++) {
      view.setUint8(i, data.at(i));
    }

causes this error

TypeError: ArrayBuffer is detached or resized
    at at (native)
    at processFrame (websocket-server.js:119:32)

but not when Chromium is the client.

It should be possible to use subarray() instead of slice() quickjs-ng/quickjs#1052
@guest271314
Copy link
Contributor Author

Updated usage, to handle mutiple requests with the same server instance

tjs tjs-websocket-server.js

import { WebSocketConnection } from "./websocket-server.js";

const decoder = new TextDecoder();

async function handleConnection(conn) {
  const writer = conn.writable.getWriter();
  const { readable: wsReadable, writable: wsWritable } = new TransformStream({}, {}, {
    highWaterMark: 1
  }),
    wsWriter = wsWritable.getWriter();
  let ws;
  for await (const value of conn.readable) {
    const request = decoder.decode(value);
    if (request.includes("Upgrade: websocket")) {
      const [key] = request.match(/(?<=Sec-WebSocket-Key: ).+/i);
      const handshake = await WebSocketConnection.hashWebSocketKey(
        key,
        writer,
      );
      ws = new WebSocketConnection(wsReadable, writer)
        .processWebSocketStream().catch((e) => {
          throw e;
        });
    } else {
      await wsWriter.ready;
      await wsWriter.write(new Uint8Array(value));
    }
  }

  console.log("WebSocket client connection closed");
  await wsWriter.close();
}
const listener = await tjs.listen("tcp", "0.0.0.0", "44818");
const { family, ip, port } = listener.localAddress;
console.log(
  `${navigator.userAgent} WebSocket server listening on family: ${family}, ip: ${ip}, port: ${port}`,
);

for await (const conn of listener) {
  try {
    console.log({ conn });
    handleConnection(conn).catch((e) => {
      console.log({ e });
    });
  } catch (e) {
    listener.close();
    console.log(e);
  }
}

Client support update.

Chromium client works. Firefox client works. Bun works, and always causes tjs to exit. Node.js (Undici) WebSocket and WebSocketStream clients, do not work, respectively. They each hang at or before open. Undici WritableStreamDefaultWriter.close() doesn't work the same as Chromium's implementation of WHATWG Streams TrasnformStream, Node.js expects ReadableStreamDefaultReader.close() to be called before writer.close(), and then fulfills the Promise with an error. Deno client doesn't work, also hangs at or around open.

The same code used in Chromium as a WebSocket server works for node, deno, bun, chrome, firefox, with the above-mentioned issues re each runtime.

@guest271314
Copy link
Contributor Author

This explains why node, deno, and bun were not working as intended

Deno

GET / HTTP/1.1
host: 127.0.0.1:44818
upgrade: websocket
connection: Upgrade
sec-websocket-key: XDSOkpJIiuT3YVzSkTuOjw==
user-agent: Deno/2.3.1+5044f2f
sec-websocket-version: 13

Node.js

GET / HTTP/1.1
host: 127.0.0.1:44818
connection: upgrade
upgrade: websocket
sec-websocket-key: Mq+14M4n9r+Lg9uPoBtPEA==
sec-websocket-version: 13
sec-websocket-extensions: permessage-deflate; client_max_window_bits
accept: */*
accept-language: *
sec-fetch-mode: websocket
user-agent: undici
pragma: no-cache
cache-control: no-cache
accept-encoding: gzip, deflate

Node.js

GET / HTTP/1.1
host: 127.0.0.1:44818
connection: upgrade
upgrade: websocket
sec-websocket-key: 48kkp4VpanJUq+FN2Y1yNw==
sec-websocket-version: 13
sec-websocket-extensions: permessage-deflate; client_max_window_bits
accept: */*
accept-language: *
sec-fetch-mode: websocket
user-agent: undici
pragma: no-cache
cache-control: no-cache
accept-encoding: gzip, deflate

Chromium

GET / HTTP/1.1
Host: 0.0.0.0:44818
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36
Upgrade: websocket
Origin: https://github.com
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Sec-WebSocket-Key: 686nTuO2/75HtVGiSHPbhA==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

Bun, using Node.js Undici WebSocketStream

GET / HTTP/1.1
host: 127.0.0.1:44818
connection: upgrade
upgrade: websocket
sec-websocket-key: Mq+14M4n9r+Lg9uPoBtPEA==
sec-websocket-version: 13
sec-websocket-extensions: permessage-deflate; client_max_window_bits
accept: */*
accept-language: *
sec-fetch-mode: websocket
user-agent: undici
pragma: no-cache
cache-control: no-cache
accept-encoding: gzip, deflate

Bun, using built-in WebSocket

GET / HTTP/1.1
Host: 127.0.0.1:44818
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: 3X8MOSPtQkqut+xMYqBC8Q==

Firefox

GET / HTTP/1.1
Host: 127.0.0.1:44818
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br, zstd
Sec-WebSocket-Version: 13
Origin: http://example.com
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-Key: pbRTE+1Gg6vowdjEBMrpdQ==
Connection: keep-alive, Upgrade
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: websocket
Sec-Fetch-Site: cross-site
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket

Notice the lowercase "upgrade". The fix when handling the request that does upgrade for the usage script is to substitute

    if (/upgrade: websocket/i.test(request)) {

for

    if (request.includes("Upgrade: websocket")) {

After checking for case-insensitive "<U|u>pgrade: websocket" node, deno, bun, chrome, firefox runtimes work as clients, both for WebSocket and WebSocketStream.

@guest271314
Copy link
Contributor Author

Updated usage.

Create server

import { WebSocketConnection } from "./assets/websocket-server.js";

const decoder = new TextDecoder();

async function handleConnection(conn) {
  const writer = conn.writable.getWriter();
  const { readable: wsReadable, writable: wsWritable } = new TransformStream(
      {},
      {},
      {
        highWaterMark: 1,
      },
    ),
    wsWriter = wsWritable.getWriter();
  let ws;
  for await (const value of conn.readable) {
    const request = decoder.decode(value);
    if (/upgrade: websocket/i.test(request)) {
      const [key] = request.match(/(?<=Sec-WebSocket-Key: ).+/i);
      const handshake = await WebSocketConnection.hashWebSocketKey(
        key,
        writer,
      );
      ws = new WebSocketConnection(wsReadable, writer);
      ws.processWebSocketStream().catch((e) => {
        throw e;
      });
      console.log(ws);
      if (!ws.incomingStream.locked) {
        ws.incomingStream.pipeTo(
          new WritableStream({
            write: async ({ opcode, payload }) => {
              if (
                opcode === ws.opcodes.CLOSE &&
                payload.buffer.byteLength === 0
              ) {
                console.log(
                  opcode,
                  payload,
                  ws.incomingStreamController,
                );
                try {
                  return await ws.close(
                    1000,
                    payload,
                  );
                } catch (e) {
                  console.log(e);
                  console.trace();
                }
              }
              await ws.writeFrame(opcode, payload);
            },
          }),
        )
          .then(() => console.log("Stream closed", ws))
          .catch((e) => {
            console.log(e);
          })
          .then(async () => {
            if (!ws.closed) {
              await Promise.allSettled([
                ws?.writable?.close(),
                ws.writer.close(),
                ws.readable.cancel(),
                ws.close(),
              ]).catch(console.log);
            }
            console.log(`Incoming WebSocketStream closed`, ws);
          });
      }
    } else {
      await wsWriter.ready;
      await wsWriter.write(new Uint8Array(value));
    }
  }

  console.log("WebSocket client connection closed");
  await wsWriter.close();
}
const listener = await tjs.listen("tcp", "0.0.0.0", "8001");
const { family, ip, port } = listener.localAddress;
console.log(
  `${navigator.userAgent} WebSocket server listening on family: ${family}, ip: ${ip}, port: ${port}`,
);

for await (const conn of listener) {
  try {
    console.log({ conn });
    handleConnection(conn).catch((e) => {
      console.log({ e });
    });
  } catch (e) {
    listener.close();
    console.log(e);
  }
}

Example usage in Chromium based browsers using WebSocketStream


var decoder = new TextDecoder();
var encoder = new TextEncoder();

var wss = new WebSocketStream("ws://127.0.0.1:8001");
console.log(wss);

var { readable, writable } = await wss.opened.catch(console.warn);

//var reader = readable.getReader();
var writer = writable.getWriter();

Promise.allSettled([writable.closed, readable.closed, wss.closed])
  .then(([,,{value:{closeCode, reason}}]) => console.log({closeCode, reason}));
/*
var data = encoder.encode("a".repeat(1024**2));
var len = 0;
var n = 0;
while (len < data.length) {
  await writer.write(data.subarray(n, n += 16384*4));
  var {value, done} = await reader.read();
  len += value.byteLength;
  console.log(value, len);
}
*/
var data = encoder.encode("a".repeat(4));
var len = 0;
var n = 0;
readable.pipeTo(
  new WritableStream({
    write(v) {
      console.log(v);
    },
    close() {
      console.log("close");
    },
    abort(reason) {
      console.log(reason);
    }
  })
).catch(console.log);

writer.write(encoder.encode("hey"));

Close connection

await writer.close()

or

wss.close({closeCode: 4999, reason: "test"})

@guest271314
Copy link
Contributor Author

Example usage using WebSocket client

var ws = new WebSocket("ws://localhost:8001");
ws.binaryType = "blob";
ws.onmessage = e => console.log(e.data)
ws.onclose = e => console.log(e);
ws.onbufferedamoutlow = (e) => console.log(e);
ws.onopen = (e) => {
  console.log(e);
  ws.send(new TextEncoder().encode("hey"));
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant