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.
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.
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]"}`);
};
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. |