Changelog
All notable changes to muxws will be documented in this file. One version number covers the Python package and the npm package; the wire subprotocol is versioned separately (muxws.v1.<codec>) and is unchanged by every release below.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
0.4.1 - 2026-10-16
Fixed
Peer.close()and a connection-level protocol error send every queued frame, including every remaining fragment of a fragmented payload, before closing the socket, bounded by the drain deadline. The socket was previously closed after a fixed number of event-loop turns, which could truncate a large reply or losegoaway(Python and TypeScript).- TypeScript: the outbound frame accounting no longer goes negative after a fragmented send, which let
close()close the socket beforegoawaywas sent. - Python: an exception from the socket adapter's
close()duringPeer.close()or a connection failure is logged as a warning and the peer is still marked closed, failing its live streams and callingon_close, as in TypeScript. - After a connection-level protocol error or a read-loop failure,
open()raisesConnectionGoingAwaywhile the queued frames are being sent, andclose()does not send a secondgoaway(WSM-CON-021; Python and TypeScript). - An
openlarger than the frame cap has its id checked for parity and monotonicity and recorded as the remote's highest open before thePAYLOAD_TOO_LARGEreset. A laterdataorreseton that id is ignored instead of ending the connection, and an over-capopenwith a wrong-parity or non-increasing id ends the connection withgoaway(PROTOCOL_ERROR)(Python and TypeScript). - An over-cap
dataorresetnaming an id that is not live is no longer answered with areset. At or below the high-water mark it is ignored (WSM-STM-002), and above it the connection ends withgoaway(PROTOCOL_ERROR)(WSM-STM-003) (Python and TypeScript). - A peer whose read loop raises sends
goaway(INTERNAL_ERROR), reports close code 1011 and closes its socket. Previously it marked itself closed and left the WebSocket open. In Python this also covers exceptions raised outside frame dispatch, which used to escapeserve()withis_openstill true. - A frame the writer cannot encode or send, when no live stream is left to fail, closes the socket with 1011 instead of only marking the peer closed (Python and TypeScript).
from_mapping/fromMappingreject envelope fields of the wrong type (for example a stringstream, a non-booleanendor a non-stringtype) withProtocolError, so such a message is a connection-level protocol error (goaway(PROTOCOL_ERROR), close 1002) in both ports. A non-stringtypewith frame logging at debug level previously stopped the Python read loop while the peer reported itself open, and closed the TypeScript connection with 1011.- A reassembled payload the codec cannot decode, or fragments of one payload that mix text and binary values, reset only that stream with
PROTOCOL_ERROR; the connection stays open (Python and TypeScript). - The WebSocket close sent by
Peer.close()or at the end of a connection-level protocol error carries at most 123 UTF-8 bytes of reason, cut at a character boundary. A longer reason madews, browserWebSocketandwebsocketsreject the close and leave the socket open. Thegoawayframe and the close report still carry the full reason. - Resetting or cancelling a stream drops the stream's queued frames that have not been sent, including the remaining fragments of a payload that is partly sent; the
resetends the fragment sequence on the receiver. Aresetreceived from the remote drops this peer's queued frames for that stream in the same way. Anopenof which nothing has been sent yet is still sent before thereset(Python and TypeScript). - A frame that fails to encode when the writer advances past the previous frame on its stream resets only that stream, with the codec's error as the reason, and drops the frames queued behind it, instead of sending them after the stream has failed (Python and TypeScript).
- An
openwhose payload the codec cannot encode fails its stream withINTERNAL_ERRORand sends nothing for the id. Aresetfor that id was sent before, and the remote, which had not seen the id, ended the connection (WSM-STM-003) (Python and TypeScript). connect()checksmax_frame_bytes/maxFrameBytesand copies the hello before dialing, so an unusable frame cap or an uncopyable hello raises without opening a socket; any other failure after the dial and before the hello closes the dialled socket (Python and TypeScript).- Python:
accept()andserve()close awebsocketsconnection whose negotiated subprotocol is notmuxws.v1.<codec>with 1008 before raisingCodecMismatch. - TypeScript:
muxws/nodeaccept()andconnect()log the negotiated-subprotocol error line on a codec mismatch detected on an open socket, as the browserconnect()and Python do. - The negotiated-subprotocol error line logged on an open-socket codec mismatch names the
VITE_MUXWS_CODECandMUXWS_CODECenvironment variables (Python and TypeScript, WSM-CDC-029). PeerRegistrykeeps one close handler per peer across reconnects; re-registering a peer after each reconnect no longer adds another handler to it (Python and TypeScript).- Python:
Peer.ping()removes its pending entry when the caller is cancelled, not only on a pong or a timeout. - TypeScript:
muxws/nodeopens aws+unix:socket path as written in the URL, as Python does: a space or non-ASCII character in the path is no longer percent-encoded,%XXis not decoded, and a trailing space stays part of the path. - Python:
MemorySocket.receive()raisesConnectionClosedon every call after the socket closes, instead of waiting forever on the second call. - TypeScript:
ConnectionClosedwith no message (or an empty one) takes its message fromreason, then"connection closed", as in Python. - TypeScript:
Peer.onErrorhandlers are called in isolation: a handler that throws is logged and the remaining handlers still run, and a throwing handler on the handler-teardown path no longer produces an unhandled rejection. Reconnectrejects a NaN initial delay, factor or max delay at construction (ValueErrorin Python,Errorin TypeScript).- An unknown frame type is logged once per peer instead of once per frame, for at most 32 distinct types (WSM-FRM-002; Python and TypeScript).
0.4.0 - 2026-10-16
Added
- Header interceptors on
Peer, each registration returning its unregister function:peer.intercept_open(interceptor)/peer.interceptOpen(interceptor): a synchronous(headers, payload)callback thatopen(),notify()andrequest()run before a stream id is allocated. It mutates a copy of the caller's headers, and the result is what theopenframe carries.peer.intercept_reply_headers(interceptor)/peer.interceptReplyHeaders(interceptor): a synchronous(headers, stream)callback run by the call that sends this peer's first frame on a stream the remote opened, including the implicit end of a handler that returns. It mutates a copy of that call's headers.peer.intercept_incoming_headers(interceptor)/peer.interceptIncomingHeaders(interceptor): a(headers, stream)callback that runs once per stream with the remote's leading headers, read-only.
connect(),accept()andserve()takeopen_interceptors,reply_headers_interceptorsandincoming_headers_interceptors(openInterceptors,replyHeadersInterceptors,incomingHeadersInterceptorsin TypeScript) and register them before the peer sends or reads a frame.- New exported types:
OpenInterceptor,ReplyHeadersInterceptor,IncomingHeadersInterceptor. ws+unix:///path/to.sock:/request-targetURLs dial a WebSocket over a Unix domain socket, in Python and frommuxws/node. The URL is split on the first:of its path in both languages.TransportUrlErrorandTransportUnsupportedError, exported from the package root: the bases of every error a transport raises for an address it cannot open or a runtime that cannot provide it. Concrete classes live in their transport's module: PythonWebsocketUrlError,WebsocketsNotInstalledError,UnixUrlError,UnixSocketsUnsupportedError; TypeScriptWsUrlError,WsNotInstalledError,UnixUrlError(muxws/node) andUnixSocketsUnsupportedError(muxws).- Python:
parse_unix_urlandUnixTargetinmuxws.transports.unix;require_websockets,verify_dialable_urlandINSTALL_HINTinmuxws.transports.websockets_.
Changed
- A malformed
ws:/wss:URL raisesWebsocketUrlError(Python) orWsUrlError(muxws/node) instead of the transport library's own exception, which is chained as the cause. A missingwebsockets/wspackage raisesWebsocketsNotInstalledError/WsNotInstalledError, naming the install command. See the migration guide. - The
websocketsextra requireswebsockets>=14. With 12 and 13 the TCP dial failed with aTypeErrorfromcreate_connection. - Fragmentation slices the UTF-8 encoding of a payload on byte offsets. Fragment boundaries are unchanged; splitting a large payload is an order of magnitude faster in both ports.
Fixed
- TypeScript:
peer.close()orstop()while the reconnect helper was dialling or waiting for the hello no longer lets that attempt establish. A socket the dial returns afterwards is closed,isOpenstays false andonReconnectdoes not fire. Python: astop()during the hello closes the socket of that attempt. - Python: cancelling the task awaiting
peer.request(..., timeout=...)sendsreset(CANCELLED)and closes the stream, as it does without a timeout. - Python:
backoff_delay()andunjittered_delay()return the cap after about 1024 failed attempts instead of raisingOverflowError, which stopped the reconnect helper.
0.3.1 - 2026-08-13
Changed
- Build only:
vite.config.tsresolves its entry points throughimport.meta.dirname. The library code is that of 0.3.0.
0.3.0 - 2026-08-13
Added
- Leading headers from the answering side.
stream.send_headers()/stream.sendHeaders()sends them on a payload-lessdataframe, andsend(),end()andreply()takeheadersfor the same purpose. The opener reads them asstream.reply_headers/stream.replyHeaders;reply_headers_arrived/replyHeadersArrivedsettles once they can no longer change, including on a stream that ends without an answer.
Changed
- Headers ride only the first stream-level frame a peer sends on a stream. Headers on a later frame reset that stream with
PROTOCOL_ERROR; the connection stays open. A secondsend_headers()raises. - TypeScript: the handshake refusals of
subprotocol.selectandBrowserSocket.connectare written throughlogger, sologger.levelgoverns them.
0.2.0 - 2026-08-10
Changed
- Package metadata only. The library code is that of 0.1.0.
0.1.0 - 2026-08-10
Added
- First release: the Python package
muxwsand the npm packagemuxws, speaking themuxws.v1wire with thejsonandmsgpackcodecs.