A zero-dependency, fully-typed implementation of the WebSocket protocol (RFC 6455) in TypeScript. Built entirely on Node.js built-ins — no runtime dependencies.
Supports per-message deflate compression (RFC 7692), SOCKS5 / HTTP CONNECT proxying, prepared (cached) messages for broadcasting, and message joining.
npm install
npm run buildimporthttpfrom'node:http';import{Upgrader,Dialer,TextMessage}from'./dist/index.js';// Serverconstupgrader=newUpgrader({enableCompression: true});constserver=http.createServer();server.on('upgrade',(req,socket,head)=>{constconn=upgrader.upgrade(req,socket,head);voidechoLoop(conn);});server.listen(8080);asyncfunctionechoLoop(conn){for(;;){const{ messageType, data }=awaitconn.readMessage();awaitconn.writeMessage(messageType,data);}}// Clientconstdialer=newDialer({handshakeTimeout: 5000});const{ conn }=awaitdialer.dial('ws://localhost:8080/');awaitconn.writeMessage(TextMessage,'Hello!');constresult=awaitconn.readMessage();console.log(result.data.toString());// "Hello!"A Conn supports one concurrent reader and one concurrent writer. This maps naturally to Node's single-threaded event loop — in practice, you run one async function for reads and one for writes.
close() and writeControl() are safe to call concurrently with everything else.
| Constant | Value | Description |
|---|---|---|
TextMessage | 1 | UTF-8 text payload |
BinaryMessage | 2 | Binary payload |
CloseMessage | 8 | Connection close |
PingMessage | 9 | Ping (keepalive) |
PongMessage | 10 | Pong response |
These are exported from both the top-level index and individually from each source module.
Performs the HTTP → WebSocket upgrade handshake. Use it inside Node's server.on('upgrade') handler.
importhttpfrom'node:http';import{Upgrader}from'websocket';constupgrader=newUpgrader({readBufferSize: 4096,writeBufferSize: 4096,subprotocols: ['chat','superchat'],enableCompression: true,checkOrigin(req){returnreq.headers.origin==='https://myapp.com';},});constserver=http.createServer();server.on('upgrade',(req,socket,head)=>{constconn=upgrader.upgrade(req,socket,head,{'Set-Cookie': 'session=abc123',});// conn.subprotocol — the negotiated subprotocol, if any// conn.isServer === true});| Option | Type | Default | Description |
|---|---|---|---|
handshakeTimeout | number | 0 (none) | Timeout in ms for the handshake write |
readBufferSize | number | 4096 | Internal read buffer size |
writeBufferSize | number | 4096 | Internal write buffer size (header space is added automatically) |
subprotocols | string[] | [] | Supported subprotocols in preference order |
checkOrigin | (req) => boolean | same-origin | Origin validation function; default rejects cross-origin |
enableCompression | boolean | false | Negotiate permessage-deflate |
errorHandler | (res, status, reason) => void | — | Custom HTTP error response for failed handshakes |
| Param | Type | Description |
|---|---|---|
req | http.IncomingMessage | The upgrade request |
socket | net.Socket | The raw TCP socket |
head | Buffer | First packet of the new stream (may be empty) |
responseHeaders | Record<string, string> | Additional headers in the 101 response (e.g. cookies) |
Throws HandshakeError if the request is not a valid WebSocket upgrade.
Convenience predicate: returns true if the request has Connection: Upgrade and Upgrade: websocket.
import{isWebSocketUpgrade,subprotocols}from'websocket';if(isWebSocketUpgrade(req)){constrequested=subprotocols(req);// ['chat', 'superchat']}Connects to a WebSocket server by performing the client-side upgrade handshake.
import{Dialer,TextMessage}from'websocket';constdialer=newDialer({handshakeTimeout: 10000,subprotocols: ['chat'],enableCompression: true,headers: {'Authorization': 'Bearer token'},});const{ conn, resp }=awaitdialer.dial('wss://echo.example.com/');// resp.statusCode === 101// resp.headers — response headers from the upgrade// conn.subprotocol — the negotiated subprotocol| Option | Type | Default | Description |
|---|---|---|---|
handshakeTimeout | number | 45000 | Timeout in ms |
readBufferSize | number | 4096 | Internal read buffer size |
writeBufferSize | number | 4096 | Internal write buffer size |
subprotocols | string[] | [] | Client subprotocol preferences |
enableCompression | boolean | false | Advertise permessage-deflate support |
tlsConfig | tls.ConnectionOptions | — | TLS settings for wss:// connections |
headers | Record<string, string> | {} | Extra HTTP headers on the upgrade request |
proxy | (req) => Promise<string | undefined> | — | Proxy resolution function |
A pre-configured singleton using Proxy: http.ProxyFromEnvironment, 45s timeout, and default buffer sizes.
import{DefaultDialer}from'websocket';const{ conn }=awaitDefaultDialer.dial('ws://localhost:8080/');// Read a complete message into a Bufferconst{ messageType, data }=awaitconn.readMessage();console.log(messageType);// TextMessage (1) or BinaryMessage (2)// Streaming read (for large messages)const{ messageType, reader }=awaitconn.nextReader();constchunks=[];for(;;){constchunk=awaitreader.read();if(chunk===null)break;chunks.push(chunk);}// Write a complete messageawaitconn.writeMessage(TextMessage,'Hello');awaitconn.writeMessage(BinaryMessage,Buffer.from([0x00,0x01]));// Stream a message (fragments automatically)constwriter=conn.nextWriter(TextMessage);writer.write('part 1');writer.write('part 2');awaitwriter.close();// Send a ping (keepalive)conn.writeControl(PingMessage,Buffer.from('heartbeat'));// Send a graceful closeconn.writeControl(CloseMessage,FormatCloseMessage(1000,'bye'));// Force-close the underlying socket (no close handshake)conn.close();conn.isServer// true if server-side// Deadlines (applied per-read / per-frame-write)conn.setReadDeadline(30000);// 30s read timeoutconn.setWriteDeadline(10000);// 10s write timeout// Read limit (auto-closes with 1009 if exceeded)conn.setReadLimit(1024*1024);// 1 MB max message// Network infoconn.localAddr// { address, family, port }conn.remoteAddr// { address, family, port }Customize how the connection responds to control frames:
// Close handler (default: echoes the close code back)conn.setCloseHandler((code,text)=>{console.log(`Peer closing: ${code}${text}`);});// Ping handler (default: auto-replies with pong)conn.setPingHandler((data)=>{console.log('Ping received:',data);});// Pong handler (default: no-op)conn.setPongHandler((data)=>{// Measure latency using custom data payloadsconstelapsed=Date.now()-parseInt(data,10);console.log(`Latency: ${elapsed}ms`);});Per-message deflate (RFC 7692) in "no context takeover" mode (fresh compression context per message).
// Server side — enable in Upgrader optionsconstupgrader=newUpgrader({enableCompression: true});// Client side — enable in Dialer optionsconstdialer=newDialer({enableCompression: true});// Per-connection togglingconn.enableWriteCompression=false;conn.setCompressionLevel(6);// -2 (HuffmanOnly) to 9 (BestCompression)Compression is negotiated during the handshake. Both sides must advertise support. Once negotiated, read decompression is automatic. Write compression can be toggled per-message via conn.enableWriteCompression.
If you need to integrate compression into custom flows:
import{compressNoContextTakeover,decompressNoContextTakeover}from'websocket';// Wrap a writerconstcompressedWriter=compressNoContextTakeover(rawWriter,1);// Wrap a readerconstdecompressedReader=decompressNoContextTakeover(rawReader);SOCKS5 and HTTP CONNECT proxies are supported on the client dial path.
import{createProxyDialer}from'websocket';constforwardDial=(host,port)=>{// standard TCP dial};constproxyDial=createProxyDialer('socks5://user:pass@proxy:1080',forwardDial);constsock=awaitproxyDial('echo.example.com',80);constproxyDial=createProxyDialer('http://proxy.corp:8080',forwardDial);constsock=awaitproxyDial('echo.example.com',443);Use the DefaultDialer or implement DialerOptions.proxy to resolve proxy URLs dynamically (e.g. from ALL_PROXY, NO_PROXY environment variables).
Convenience wrappers for JSON serialization over WebSocket text messages:
import{writeJSON,readJSON}from'websocket';awaitwriteJSON(conn,{type: 'greeting',body: 'hello'});constmsg=awaitreadJSON(conn);// msg === { type: 'greeting', body: 'hello' }Cache wire-format frame data for broadcasting the same message to many connections. Each unique combination of (server/client, compress, level) is encoded once:
import{PreparedMessage,TextMessage}from'websocket';constpm=newPreparedMessage(TextMessage,JSON.stringify({event: 'tick',ts: Date.now()}));// Broadcast to many connections without re-encodingfor(constconnofconnections){awaitconn.writePreparedMessage(pm);}Concatenate consecutive WebSocket messages into a single readable stream with a delimiter:
import{joinMessages}from'websocket';constreader=joinMessages(conn,'\n');constchunks=[];for(;;){constchunk=awaitreader.read();if(chunk===null)break;chunks.push(chunk);}constfull=Buffer.concat(chunks).toString();Thrown when the peer sends a close frame. Carries the close code and optional text:
import{CloseError,isCloseError,isUnexpectedCloseError}from'websocket';try{awaitconn.readMessage();}catch(err){if(isCloseError(err)){console.log(`Closed: ${err.code}`);}if(isUnexpectedCloseError(err,1000,1001)){// Not a normal closure — log and investigateconsole.error('Abnormal close:',err);}}| Constant | Code | Meaning |
|---|---|---|
CloseNormalClosure | 1000 | Normal closure |
CloseGoingAway | 1001 | Endpoint going away |
CloseProtocolError | 1002 | Protocol error |
CloseUnsupportedData | 1003 | Received data type not supported |
CloseNoStatusReceived | 1005 | No status (reserved) |
CloseAbnormalClosure | 1006 | Abnormal (reserved) |
CloseInvalidFramePayloadData | 1007 | Invalid payload data |
ClosePolicyViolation | 1008 | Policy violation |
CloseMessageTooBig | 1009 | Message too big |
CloseMandatoryExtension | 1010 | Extension expected |
CloseInternalServerErr | 1011 | Internal server error |
CloseServiceRestart | 1012 | Service restart |
CloseTryAgainLater | 1013 | Try again later |
CloseTLSHandshake | 1015 | TLS handshake failure |
| Error | When |
|---|---|
ErrCloseSent | Write attempted after a close frame was sent |
ErrReadLimit | Message exceeds setReadLimit |
Builds a close frame payload (2-byte big-endian code + UTF-8 text):
import{FormatCloseMessage,CloseNormalClosure}from'websocket';conn.writeControl(CloseMessage,FormatCloseMessage(CloseNormalClosure,'bye'));For advanced use cases (custom frame handling, protocol debugging):
import{computeAcceptKey,// SHA-1(challengeKey + GUID), base64generateChallengeKey,// 16 random bytes, base64parseExtensions,// Parse Sec-WebSocket-Extensions headerparseFrameHeader,// Parse raw frame header bytes into structured datamaskBytes,// XOR bytes with rotating 4-byte keytokenListContainsValue,// RFC 2616 1#token header value check}from'websocket';constkey=computeAcceptKey('dGhlIHNhbXBsZSBub25jZQ==');// "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="importhttpfrom'node:http';import{Upgrader,Dialer,Conn,TextMessage,CloseMessage,CloseNormalClosure,CloseError,FormatCloseMessage,isCloseError,}from'websocket';// ── Server ────────────────────────────────────────────constupgrader=newUpgrader({enableCompression: true});constserver=http.createServer();server.on('upgrade',(req,socket,head)=>{constconn=upgrader.upgrade(req,socket,head);console.log('New connection, subprotocol:',conn.subprotocol);handleClient(conn).catch(()=>{});});asyncfunctionhandleClient(conn: Conn){try{for(;;){const{ messageType, data }=awaitconn.readMessage();console.log('Received:',data.toString());awaitconn.writeMessage(messageType,data);// echo}}catch(err){if(isCloseError(err)){console.log('Client closed:',err.code);}}}server.listen(8080,()=>console.log('Listening on :8080'));// ── Client ────────────────────────────────────────────constdialer=newDialer({handshakeTimeout: 5000,enableCompression: true,});const{ conn }=awaitdialer.dial('ws://localhost:8080/');// Send a messageawaitconn.writeMessage(TextMessage,'Hello from client!');// Read the echoconst{ data }=awaitconn.readMessage();console.log('Echo:',data.toString());// Graceful closeawaitconn.writeControl(CloseMessage,FormatCloseMessage(CloseNormalClosure,'done'));npm test# all tests (75)# or individually:
npm run test:e2e # end-to-end integration onlyBuilt with Node's native test runner. No test framework dependencies. Tests cover frame parsing, masking, compression round-trips, server upgrade validation, client handshake, proxying, JSON serialization, prepared message caching, message joining, and end-to-end echo flows.