WebSocket integration
The independently versioned
github.com/spice-framework/starter-websocket
module provides an explicit RFC 6455 boundary backed by
github.com/coder/websocket. It does not add an annotation language, global
connection hub, hidden goroutine pool, authentication scheme, or application
message protocol.
go get github.com/spice-framework/starter-websocket@latestServer
NewHandler returns an ordinary http.Handler for a generated or manually
registered Spice route:
handler, err := websocketstarter.NewHandler( websocketstarter.ServerConfig{ Authenticate: func(ctx context.Context, request *http.Request) error { return authorizer.Check(ctx, request.Header.Get("Authorization")) }, Subprotocols: []string{"petclinic.events.v1"}, MaxMessageBytes: 1 << 20, MaxConnections: 256, }, func(ctx context.Context, connection *websocketstarter.Connection) error { messageType, payload, err := connection.Read(ctx) if err != nil { return err } return connection.Write(ctx, messageType, payload) }, observer,)TLS and authentication are required by default and remain owned by the
surrounding http.Server and application callback. Anonymous access and plain
HTTP loopback development are separate explicit decisions. Browser origins are
same-host only by default.
Cross-origin host patterns are explicit, bounded, deduplicated, and validated;
* is rejected. Disabling origin verification requires the separately visible
AllowAnyOrigin flag and is legal only with authentication.
Message size defaults to 1 MiB and cannot exceed 16 MiB. Active sessions default to 256 and cannot exceed 4096. Capacity is acquired before the upgrade, so excess handshakes receive HTTP 503 rather than consuming an unbounded socket. Compression is disabled by default; the opt-in mode uses no context takeover and a bounded threshold. The caller owns the session handler and all authentication, authorization, heartbeat, fan-out, persistence, and application-level backpressure.
When a session finishes, the handler performs a bounded close handshake.
Lifecycle cancellation uses going away; application failures use a generic
internal-error close without leaking error text. If the handshake cannot finish
within the configured timeout, the socket is force-closed.
Client
Dial requires a wss://host:port/path URL and verified TLS 1.2 or newer by
default:
connection, response, cleanup, err := websocketstarter.Dial( ctx, websocketstarter.ClientConfig{ URL: "wss://events.example.internal:8443/owners", Subprotocols: []string{"petclinic.events.v1"}, Authorization: "Bearer " + token, }, observer,)Credentials in URL user-info are rejected. Fragments, missing ports, reserved
WebSocket headers, noncanonical header names, CR/LF injection, oversized
headers, old TLS, and disabled certificate verification fail before dialing.
Caller headers and TLS configuration are defensively cloned. Environmental HTTP
proxies are not consulted. ws:// is accepted only with
AllowInsecure: true, which cannot be combined with TLS configuration.
The returned Connection provides bounded Read, Write, Ping, negotiated
subprotocol inspection, and idempotent context-aware Close. Canceling an
active read follows the native library contract and closes the connection.
Ping requires a concurrent reader so pong control frames can be processed.
Observability
Observers receive only direction, negotiated subprotocol, terminal normal/canceled/failed outcome, and duration. URLs, headers, origins, peer addresses, close reasons, credentials, and message payloads are deliberately absent.
The starter repository owns the canonical dependency review, support policy, compatibility manifest, and real local TLS acceptance evidence. This core document remains the ecosystem composition guide.