wsconn

package module
v0.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 2, 2026 License: MIT Imports: 16 Imported by: 0

README

wsconn

A minimal RFC 6455 WebSocket implementation: a client-side Dial, a server-side Accept, and text-message Read/Write/Close on the resulting Conn. No third-party dependency — dial, the HTTP upgrade handshake, and frame read/write are all implemented directly on top of net/net/http.

Deliberately narrow: no compression, subprotocol negotiation, or binary-message support. It exists to serve two consumers whose protocols are both plain JSON over text frames — haws (Home Assistant's WebSocket API) and zwavejs-go (zwave-js-server's WebSocket API) — not to be a general-purpose WebSocket library. If you need any of those, use a real one.

Install

go get github.com/ryanjohnsontv/wsconn

Client

conn, err := wsconn.Dial(ctx, "ws://192.168.1.10:8123/api/websocket")
if err != nil {
    log.Fatal(err)
}
defer conn.Close(wsconn.StatusNormalClosure, "done")

if err := conn.Write(ctx, []byte(`{"type":"ping"}`)); err != nil {
    log.Fatal(err)
}

msg, err := conn.Read(ctx) // blocks for one complete message

Dial accepts URLs in any scheme, as well as protocol-relative (//host/path) and bare (host:port/path) forms. HTTPS and WSS inputs use WSS; all other inputs use WS.

Server

Accept upgrades an incoming request by hijacking the connection — for a fake server in tests, not a production listener:

http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
    conn, err := wsconn.Accept(w, r)
    if err != nil {
        return // Accept already wrote the HTTP error response
    }
    defer conn.Close(wsconn.StatusNormalClosure, "")
    ...
})

Reading

Read blocks until one complete text message arrives, transparently reassembling fragmented frames and replying to pings along the way. It returns ErrConnClosed once the peer's close frame has been received and the close handshake completes. Only one goroutine may call Read at a time; Write and Close are safe to call concurrently with Read and with each other.

ctx bounds each individual Read/Write call — cancellation sets a deadline on the underlying connection, so a canceled Read doesn't leave the connection itself unusable for the next call, only the current one.

Design constraints

  • Text messages only. Every Write sends opText; there's no binary frame support, since neither consumer's protocol needs it.
  • 256MiB per-message cap, generous enough for Home Assistant's get_states on a large install and zwave-js-server's start_listening node/value dump on a real network. Not configurable — if a consumer ever needs more, that's a real design conversation, not a quick constant bump.
  • No compression or subprotocol negotiation. Neither protocol this serves uses either.

Consumers

Package Uses
haws Dial for the real client; Accept for hawstest's fake Home Assistant server
zwavejs-go (clients/zwavejs) Dial only

If you're adding a third consumer, make sure its protocol is actually plain-JSON-over-text-frames first — that assumption is load-bearing throughout this package, not incidental.

Feedback

Issues, questions, and PRs are welcome — this is early, and real-world feedback (what broke, what's confusing, what's missing) is genuinely useful.

Documentation

Overview

Package wsconn is a minimal RFC 6455 WebSocket implementation: a client-side Dial, a server-side Accept, and text-message Read/Write/Close on the resulting Conn. Shared by haws (both sides — Dial for the real client, Accept for hawstest's fake server) and zwavejs-go (Dial only), so neither depends on a third-party websocket library. No compression, subprotocol negotiation, or binary-message support — nothing either consumer's JSON-over-text protocol needs.

Index

Constants

This section is empty.

Variables

View Source
var ErrConnClosed = errors.New("wsconn: connection closed")

ErrConnClosed is returned by Read once the peer has closed the connection (a close frame was received and the close handshake completed).

Functions

This section is empty.

Types

type Conn

type Conn struct {
	// contains filtered or unexported fields
}

Conn is one WebSocket connection, usable from either the client (Dial) or server (Accept) side. A single goroutine may call Read at a time (it isn't reentrant), matching haws's own single-reader-loop design; Write and Close are safe to call concurrently with Read and with each other.

func Accept

func Accept(w http.ResponseWriter, r *http.Request) (*Conn, error)

Accept upgrades an incoming HTTP request to a server-side WebSocket connection by hijacking the connection. On a malformed or non-upgrade request it writes an HTTP error response itself and returns a non-nil error; the caller shouldn't write to w afterward either way. Used only by hawstest's fake Home Assistant server.

func Dial

func Dial(ctx context.Context, urlStr string) (*Conn, error)

Dial opens a client WebSocket connection to urlStr, performing the HTTP upgrade handshake and returning once the connection is ready to Read/Write. It accepts ws(s), http(s), protocol-relative, and bare host URLs. HTTP URLs are converted to their equivalent WebSocket scheme. ctx bounds the whole dial+handshake.

func (*Conn) Close

func (c *Conn) Close(code StatusCode, reason string) error

Close sends a close frame (code and reason, truncated to fit a control frame if needed) and closes the underlying connection. Safe to call more than once, or concurrently with a Read that's mid-close-handshake with the peer — only the first close (whichever happens first) actually writes a frame and closes the socket.

func (*Conn) Read

func (c *Conn) Read(ctx context.Context) ([]byte, error)

Read blocks until one complete text message arrives (reassembling fragmented frames, and transparently handling ping/pong control frames along the way), or ctx is done, or the peer closes the connection (returning ErrConnClosed).

func (*Conn) Write

func (c *Conn) Write(ctx context.Context, data []byte) error

Write sends data as a single unmasked-or-masked (per role) text frame.

type StatusCode

type StatusCode uint16

StatusCode is a WebSocket close status code (RFC 6455 §7.4).

const (
	// StatusNormalClosure indicates a clean, expected close.
	StatusNormalClosure StatusCode = 1000
	// StatusAbnormalClosure has no defined on-the-wire meaning (the RFC
	// reserves it for reporting a close that never got a close frame at
	// all) — haws only ever sends it to simulate a dropped connection in
	// tests (see hawstest.Server.DropConnections).
	StatusAbnormalClosure StatusCode = 1006
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL