Skip to content

A re-export of WebSocket from undici for compatibility with Bun.

License

Notifications You must be signed in to change notification settings

Vexcited/tcp-websocket

Folders and files

NameName
Last commit message
Last commit date

Latest commit

7b78efd · Feb 28, 2025

History

46 Commits
Feb 28, 2025
Feb 28, 2025
Feb 28, 2025
Feb 28, 2025
Sep 10, 2023
Feb 28, 2025
Feb 28, 2025
Feb 28, 2025
Feb 28, 2025
Feb 28, 2025
Feb 28, 2025
Feb 28, 2025
Feb 28, 2025

Repository files navigation

tcp-websocket

Was originally made to resolve this Bun issue: oven-sh/bun#4529.

Instead of using built-in WebSocket, we re-use the WebSocket from undici and export it correctly in this package with the typings needed.

Why not directly use undici ?

Bun patches undici imports under the hood, resulting in it being broken and useless to resolve the issue, see undici.js and Undici.cpp.

For example, if we write the following code with Bun (so, using the native WebSocket implementation) :

const ws = new WebSocket("ws://localhost:8080", {
  headers: {
    "User-Agent": "hello",
    "X-My-HeADeR": "world",
    authorization: "Bearer Hello!",
    origin: "http://localhost:8080"
  }
});
Server code (uses Node.js and ws package)
import { createServer } from 'http';
import { WebSocketServer } from 'ws';

const server = createServer();

const wss = new WebSocketServer({ server });
wss.on('connection', (_, req) => {
  const headers = {};
  // We use `req.rawHeaders` to see the case-sensitive headers.
  for (let i = 0; i < req.rawHeaders.length; i += 2) {
    headers[req.rawHeaders[i]] = req.rawHeaders[i + 1];
  }

  console.log(headers);
});

server.listen(8080);

We get the following headers on the server :

{
  Host: 'localhost:8080',
  Connection: 'Upgrade',
  Upgrade: 'websocket',
  'Sec-WebSocket-Version': '13',
  'Sec-WebSocket-Key': 'RzRIg/gRTDCqu0rhOXe6OQ==',
  Authorization: 'Bearer Hello!',
  Origin: 'http://localhost:8080',
  'User-Agent': 'hello',
  'X-My-HeADeR': 'world'
}

As you can see, the headers are not the same as the ones we provided. authorization and origin got uppercased.

Let's now try to use WebSocket from undici directly :

import { WebSocket } from "undici";

const ws = new WebSocket("ws://localhost:8080", {
  headers: {
    "User-Agent": "hello",
    "X-My-HeADeR": "world",
    authorization: "Bearer Hello!",
    origin: "http://localhost:8080"
  }
});

We get the following headers on the server :

{
  Host: 'localhost:8080',
  Connection: 'Upgrade',
  Upgrade: 'websocket',
  'Sec-WebSocket-Version': '13',
  'Sec-WebSocket-Key': 'ND4bTHtlQ/e/CwzhJp6mFA==',
  Authorization: 'Bearer Hello!',
  Origin: 'http://localhost:8080',
  'User-Agent': 'hello',
  'X-My-HeADeR': 'world'
}

We get exactly the same output ! This is because Bun internally redirects the WebSocket import from undici to the native WebSocket implementation.

Let's prevent this by tweaking the imports :

import { WebSocket } from "undici/lib/web/websocket/websocket.js";

const ws = new WebSocket("ws://localhost:8080", {
  headers: {
    "User-Agent": "hello",
    "X-My-HeADeR": "world",
    authorization: "Bearer Hello!",
    origin: "http://localhost:8080"
  }
});

Now, we get the following headers on the server :

{
  host: 'localhost:8080',
  connection: 'upgrade',
  upgrade: 'websocket',
  'User-Agent': 'hello',
  'X-My-HeADeR': 'world',
  authorization: 'Bearer Hello!',
  origin: 'http://localhost:8080',
  'sec-websocket-key': 'K8JDPp71F1TYDKXujpqoxw==',
  'sec-websocket-version': '13',
  'sec-websocket-extensions': 'permessage-deflate; client_max_window_bits',
  accept: '*/*',
  'accept-language': '*',
  'sec-fetch-mode': 'websocket',
  pragma: 'no-cache',
  'cache-control': 'no-cache',
  'accept-encoding': 'gzip, deflate'
}

It works as expected !

Now the issue is that we're missing typings, this is what this package is for.

Usage

npm add tcp-websocket
yarn add tcp-websocket
pnpm add tcp-websocket
bun add tcp-websocket
import WebSocket from "tcp-websocket";

const ws = new WebSocket("wss://ws.postman-echo.com/raw");
console.info("connecting...")

ws.onopen = () => {
  console.info("[open]: connected");
  ws.send("hello world!");

  console.info("[open]: will close in 5 seconds...");
  setTimeout(() => ws.close(), 5_000);
};

ws.onmessage = (event) => {
  console.info("[message]:", event.data);
};

ws.onclose = (event) => {
  console.info(`[close(${event.code})]: ${event.reason || "[no reason provided]"}`);
};

Why not use those libraries instead ?

Packages using node:http or node:https to make the request handshake will fail in Bun since their implementation also tweaks the headers.

Package name on NPM Issues with Bun
ws Uses the node:http and node:https to make the request handshake. Source
websocket Uses the node:http and node:https to make the request handshake. Source
websocket-stream Uses the ws package internally, see ws. Source
websocket-driver Works as expected with Bun and others, but last update was 3 years ago with no TS declarations and ES5 syntax.