Skip to content

Repository files navigation

WebSocket

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.

Installation

npm install
npm run build

Quick Start

Echo Server + Client

importhttpfrom'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!"

Core Concepts

Concurrency Model

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.

Message Types

ConstantValueDescription
TextMessage1UTF-8 text payload
BinaryMessage2Binary payload
CloseMessage8Connection close
PingMessage9Ping (keepalive)
PongMessage10Pong response

These are exported from both the top-level index and individually from each source module.


Server API

Upgrader

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});

UpgraderOptions

OptionTypeDefaultDescription
handshakeTimeoutnumber0 (none)Timeout in ms for the handshake write
readBufferSizenumber4096Internal read buffer size
writeBufferSizenumber4096Internal write buffer size (header space is added automatically)
subprotocolsstring[][]Supported subprotocols in preference order
checkOrigin(req) => booleansame-originOrigin validation function; default rejects cross-origin
enableCompressionbooleanfalseNegotiate permessage-deflate
errorHandler(res, status, reason) => voidCustom HTTP error response for failed handshakes

upgrade(req, socket, head, responseHeaders?)

ParamTypeDescription
reqhttp.IncomingMessageThe upgrade request
socketnet.SocketThe raw TCP socket
headBufferFirst packet of the new stream (may be empty)
responseHeadersRecord<string, string>Additional headers in the 101 response (e.g. cookies)

Throws HandshakeError if the request is not a valid WebSocket upgrade.

isWebSocketUpgrade(req)

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']}

Client API

Dialer

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

DialerOptions

OptionTypeDefaultDescription
handshakeTimeoutnumber45000Timeout in ms
readBufferSizenumber4096Internal read buffer size
writeBufferSizenumber4096Internal write buffer size
subprotocolsstring[][]Client subprotocol preferences
enableCompressionbooleanfalseAdvertise permessage-deflate support
tlsConfigtls.ConnectionOptionsTLS settings for wss:// connections
headersRecord<string, string>{}Extra HTTP headers on the upgrade request
proxy(req) => Promise<string | undefined>Proxy resolution function

DefaultDialer

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/');

Connection API

Reading Messages

// 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);}

Writing Messages

// 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();

Control Frames

// 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();

Connection State

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 }

Control Handlers

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

Compression

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);

Proxy Support

SOCKS5 and HTTP CONNECT proxies are supported on the client dial path.

SOCKS5

import{createProxyDialer}from'websocket';constforwardDial=(host,port)=>{// standard TCP dial};constproxyDial=createProxyDialer('socks5://user:pass@proxy:1080',forwardDial);constsock=awaitproxyDial('echo.example.com',80);

HTTP CONNECT

constproxyDial=createProxyDialer('http://proxy.corp:8080',forwardDial);constsock=awaitproxyDial('echo.example.com',443);

Environment-Variable Proxying

Use the DefaultDialer or implement DialerOptions.proxy to resolve proxy URLs dynamically (e.g. from ALL_PROXY, NO_PROXY environment variables).


JSON Helpers

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' }

Prepared Messages

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);}

Message Joining

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();

Error Handling

CloseError

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);}}

Close Status Codes

ConstantCodeMeaning
CloseNormalClosure1000Normal closure
CloseGoingAway1001Endpoint going away
CloseProtocolError1002Protocol error
CloseUnsupportedData1003Received data type not supported
CloseNoStatusReceived1005No status (reserved)
CloseAbnormalClosure1006Abnormal (reserved)
CloseInvalidFramePayloadData1007Invalid payload data
ClosePolicyViolation1008Policy violation
CloseMessageTooBig1009Message too big
CloseMandatoryExtension1010Extension expected
CloseInternalServerErr1011Internal server error
CloseServiceRestart1012Service restart
CloseTryAgainLater1013Try again later
CloseTLSHandshake1015TLS handshake failure

Sentinel Errors

ErrorWhen
ErrCloseSentWrite attempted after a close frame was sent
ErrReadLimitMessage exceeds setReadLimit

FormatCloseMessage(code, text?)

Builds a close frame payload (2-byte big-endian code + UTF-8 text):

import{FormatCloseMessage,CloseNormalClosure}from'websocket';conn.writeControl(CloseMessage,FormatCloseMessage(CloseNormalClosure,'bye'));

Protocol-Level Utilities

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="

Complete Example

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'));

Testing

npm test# all tests (75)# or individually:
npm run test:e2e # end-to-end integration only

Built 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.

About

Implementation of the WebSocket Protocol

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages