Help us shape the future of Go! Take the 2026 Go Developer Survey

websocket

package module
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Sep 24, 2026 License: MIT Imports: 24 Imported by: 0

README

deps.dev License License Stay with Ukraine

websocket

websocket implements the WebSocket protocol (RFC 6455) on top of Go's standard library, with no third-party dependencies. It gives you a server-side upgrade, a client-side dial, the permessage-deflate extension (RFC 7692) and subprotocol negotiation.

Because the package is named websocket, the natural name for a connection is ws:

ws, err := websocket.Upgrade(w, r)

It composes with the rest of net/http, including the goloop/mux router and goloop/middlewares (whose Compress correctly leaves upgrade requests alone).

Features

  • Server Upgrade (one-off or a reusable Upgrader) and client Dial.
  • Text and binary messages, with ReadMessage/WriteMessage and NextReader/NextWriter (reads stream; a write buffers the message and sends it on Close).
  • Automatic ping/pong and a proper closing handshake, with overridable handlers.
  • permessage-deflate compression with a decompression-bomb guard.
  • Subprotocol negotiation, per-message size limits, deadlines.
  • ReadJSON/WriteJSON convenience.
  • Same-origin by default, to block cross-site WebSocket hijacking.

Installation

go get -u github.com/goloop/websocket
import "github.com/goloop/websocket"

Requires Go 1.25 or newer.

Quick start

Server (echo):

func handler(w http.ResponseWriter, r *http.Request) {
    ws, err := websocket.Upgrade(w, r)
    if err != nil {
        return // an HTTP error has already been written
    }
    defer ws.Close()
    for {
        mt, data, err := ws.ReadMessage()
        if err != nil {
            break
        }
        if err := ws.WriteMessage(mt, data); err != nil {
            break
        }
    }
}

Reusable configuration:

up := websocket.NewUpgrader(
    websocket.WithReadLimit(1<<20),
    websocket.WithCompression(),
    websocket.WithSubprotocols("chat"),
)
ws, err := up.Upgrade(w, r)

Client:

ws, resp, err := websocket.Dial(ctx, "wss://example.com/ws")
if err != nil {
    log.Fatal(err)
}
defer ws.Close()
ws.WriteMessage(websocket.TextMessage, []byte("hello"))

Security notes

  • Origin. The default Upgrader accepts only same-origin requests. To serve a public endpoint or a browser on another origin, pass WithOriginChecker with your own policy.
  • Message size. WithReadLimit (default 32 MiB) caps a single message; the limit applies to the decompressed size, so it also bounds permessage-deflate.
  • Compression. permessage-deflate uses "no context takeover" per message.
  • Deadlines. Set SetReadDeadline/SetWriteDeadline to protect against slow peers. SetWriteDeadline also ends a write in progress, and the deadline given to WriteControl bounds its wait for one.
  • Truncated messages. A connection that ends between fragments fails with io.ErrUnexpectedEOF; a partial message is never returned as a whole one.
  • Dial budget. The handshake is bounded in time and in bytes, and answers context cancellation at once.

Concurrency

A connection supports one concurrent reader and one concurrent writer: ReadMessage may run in one goroutine while WriteMessage runs in another. WriteControl may be called concurrently with a writer. Do not run two readers or two writers at once.

Documentation

Contributing

Contributions are welcome. Please run go test ./..., go test -race ./..., go vet ./... and gofmt -l . before submitting a pull request.

License

websocket is released under the MIT License. See LICENSE.

Documentation

Overview

Package websocket implements the WebSocket protocol (RFC 6455) on top of the standard library, with no third-party dependencies. It provides a server-side upgrade, a client-side dial, the permessage-deflate extension (RFC 7692) and subprotocol negotiation.

A connection is represented by Conn. Because the package name is websocket, the natural variable name for a connection is ws:

ws, err := websocket.Upgrade(w, r)
if err != nil {
    return
}
defer ws.Close()
for {
    mt, data, err := ws.ReadMessage()
    if err != nil {
        break
    }
    if err := ws.WriteMessage(mt, data); err != nil {
        break
    }
}

Server upgrade. Use the package Upgrade for a one-off, or NewUpgrader for a reusable configuration:

up := websocket.NewUpgrader(websocket.WithReadLimit(1 << 20))
ws, err := up.Upgrade(w, r)

By default the upgrade only accepts same-origin requests, which guards against cross-site WebSocket hijacking. Allow other origins explicitly with WithOriginChecker.

Client dial. Dial connects to a server, negotiating TLS for wss URLs:

ws, resp, err := websocket.Dial(ctx, "wss://example.com/ws")

Concurrency. A connection supports one concurrent reader and one concurrent writer. That is, ReadMessage or NextReader may run in one goroutine while WriteMessage or NextWriter runs in another, but you must not call two readers or two writers at the same time. WriteControl may be called concurrently with a writer.

Control frames. Ping, pong and close frames are handled by the reader: a ping is answered with a pong automatically, and a close starts the closing handshake. Install SetPingHandler, SetPongHandler or SetCloseHandler to observe or override this.

Example

Example runs a tiny echo server and a client against it.

package main

import (
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"

	"github.com/goloop/websocket"
)

func main() {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ws, err := websocket.Upgrade(w, r,
			websocket.WithOriginChecker(func(*http.Request) bool { return true }))
		if err != nil {
			return
		}
		defer ws.Close()
		mt, data, err := ws.ReadMessage()
		if err != nil {
			return
		}
		_ = ws.WriteMessage(mt, data)
	}))
	defer srv.Close()

	url := "ws" + strings.TrimPrefix(srv.URL, "http")
	ws, _, err := websocket.Dial(context.Background(), url)
	if err != nil {
		fmt.Println("dial:", err)
		return
	}
	defer ws.Close()

	if err := ws.WriteMessage(websocket.TextMessage, []byte("ping")); err != nil {
		fmt.Println("write:", err)
		return
	}
	_, data, err := ws.ReadMessage()
	if err != nil {
		fmt.Println("read:", err)
		return
	}
	fmt.Println(string(data))
}
Output:
ping

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrReadLimit means a message exceeded [Conn.SetReadLimit]. The
	// connection is closed with 1009.
	ErrReadLimit = errors.New("websocket: read limit exceeded")

	// ErrProtocol means the peer broke the framing protocol. Every such
	// violation matches it, and the connection is closed with 1002; the
	// error's own text says which rule was broken.
	ErrProtocol = errors.New("websocket: protocol error")

	// ErrConfig means an option was given a value this package cannot use.
	// It surfaces from Upgrade or Dial before anything reaches the network,
	// rather than being quietly replaced by a default.
	ErrConfig = errors.New("websocket: invalid configuration")

	// ErrStaleReader means the reader belongs to a message that is over: a
	// later NextReader or ReadMessage has moved the connection on. Reading
	// from it would return bytes of a different message.
	ErrStaleReader = errors.New("websocket: reader belongs to a finished message")

	// The rest are programming errors: the arguments of a call were wrong,
	// so nothing is written and the connection is left as it was.
	ErrWriteClosed     = errors.New("websocket: write to closed message writer")
	ErrBadControl      = errors.New("websocket: not a control frame type")
	ErrControlTooBig   = errors.New("websocket: control frame payload too large")
	ErrBadWriteType    = errors.New("websocket: not a data message type")
	ErrBadClosePayload = errors.New(
		"websocket: close payload has a reserved code or invalid reason")
)

Errors a caller can act on. They are values rather than messages so that a read or write loop can tell a message that was too big from a peer that broke the protocol from a call that was simply wrong, with errors.Is and without matching on text.

View Source
var ErrBadHandshake = errors.New("websocket: bad handshake")

ErrBadHandshake is returned by Dial when the server does not complete the WebSocket handshake. The returned *http.Response carries the server's reply so the caller can inspect the status and headers.

View Source
var ErrCloseSent = errors.New("websocket: close sent")

ErrCloseSent is returned by a write after the closing handshake has started.

View Source
var ErrHandshakeTooLarge = errors.New("websocket: handshake response too large")

ErrHandshakeTooLarge is returned by Dial when the server's handshake response exceeds the byte budget set by WithDialHandshakeLimit. Nothing bounds an HTTP response read this way on its own, so without a budget a server could spend the client's memory before the handshake timeout was anywhere near expiring.

View Source
var ErrWriteLimit = errors.New("websocket: write limit exceeded")

ErrWriteLimit is returned when a message would exceed the limit set by Conn.SetWriteLimit. Nothing is written and the connection stays usable, so a caller can send something smaller instead.

Functions

func IsCloseError

func IsCloseError(err error, codes ...CloseCode) bool

IsCloseError reports whether err is a *CloseError with one of the given codes.

func IsUnexpectedCloseError

func IsUnexpectedCloseError(err error, expected ...CloseCode) bool

IsUnexpectedCloseError reports whether err is a *CloseError whose code is not one of the expected ones. It is the usual way to tell a clean shutdown from a surprising one in a read loop.

func IsWebSocketUpgrade

func IsWebSocketUpgrade(r *http.Request) bool

IsWebSocketUpgrade reports whether r is a WebSocket upgrade request.

func Subprotocols

func Subprotocols(r *http.Request) []string

Subprotocols returns the subprotocols listed in the request's Sec-WebSocket-Protocol header. The header may be split across several field lines, all of which are considered.

Types

type CloseCode

type CloseCode uint16

CloseCode is a WebSocket close status code (RFC 6455 section 7.4).

const (
	CloseNormalClosure           CloseCode = 1000
	CloseGoingAway               CloseCode = 1001
	CloseProtocolError           CloseCode = 1002
	CloseUnsupportedData         CloseCode = 1003
	CloseNoStatusReceived        CloseCode = 1005
	CloseAbnormalClosure         CloseCode = 1006
	CloseInvalidFramePayloadData CloseCode = 1007
	ClosePolicyViolation         CloseCode = 1008
	CloseMessageTooBig           CloseCode = 1009
	CloseMandatoryExtension      CloseCode = 1010
	CloseInternalServerErr       CloseCode = 1011
	CloseServiceRestart          CloseCode = 1012
	CloseTryAgainLater           CloseCode = 1013
	CloseBadGateway              CloseCode = 1014
	CloseTLSHandshake            CloseCode = 1015
)

Close status codes defined by RFC 6455. Codes 1005, 1006 and 1015 are reserved and must not be sent on the wire; they only appear as locally generated values.

type CloseError

type CloseError struct {
	Code CloseCode
	Text string
	// contains filtered or unexported fields
}

CloseError records how the connection ended.

Usually it is the close frame the peer sent, with its code and reason. It is also how an abnormal end is reported: a connection that dropped without a closing handshake yields code 1006 with the underlying error kept as the cause, so errors.Is still finds io.EOF or io.ErrUnexpectedEOF beneath it. 1006 is a local observation and is never put on the wire.

func (*CloseError) Error

func (e *CloseError) Error() string

Error implements the error interface, formatting the close code and, when present, the reason text the peer sent with the close frame.

func (*CloseError) Unwrap added in v0.5.0

func (e *CloseError) Unwrap() error

Unwrap returns the error behind an abnormal close, so a caller can still match io.EOF or io.ErrUnexpectedEOF through it.

type Conn

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

Conn is a WebSocket connection. It carries a single logical stream of messages in each direction over a hijacked net.Conn.

A Conn supports one concurrent reader and one concurrent writer; see the package documentation. WriteControl may be called concurrently with a writer.

func Dial

func Dial(ctx context.Context, urlStr string, opts ...DialOption) (*Conn, *http.Response, error)

Dial connects to a WebSocket server. The URL scheme must be ws or wss; wss connections are negotiated over TLS. On success it returns the connection and the server's handshake response. On a non-101 reply it returns ErrBadHandshake together with the response.

func Upgrade

func Upgrade(w http.ResponseWriter, r *http.Request, opts ...Option) (*Conn, error)

Upgrade is a convenience wrapper that upgrades r with a one-off configuration.

func (*Conn) Close

func (c *Conn) Close() error

Close closes the underlying network connection without performing the closing handshake. For a graceful shutdown, call CloseWithStatus first, then Close.

func (*Conn) CloseWithStatus

func (c *Conn) CloseWithStatus(code CloseCode, reason string) error

CloseWithStatus sends a close frame with the given code and reason. It does not close the network connection; the peer is expected to answer with its own close, after which the reader returns a *CloseError and the caller should call Close. A code of 0 sends an empty close payload.

func (*Conn) CompressionEnabled added in v0.6.0

func (c *Conn) CompressionEnabled() bool

CompressionEnabled reports whether permessage-deflate was negotiated for this connection. Asking for compression is not the same as getting it: the other end may decline, or offer terms this package will not accept, and until now there was no way to tell which had happened.

func (*Conn) LocalAddr

func (c *Conn) LocalAddr() net.Addr

LocalAddr returns the local network address.

func (*Conn) NetConn

func (c *Conn) NetConn() net.Conn

NetConn returns the underlying network connection. Reading from or writing to it directly will corrupt the WebSocket stream; it is an escape hatch for deadlines, addresses and similar low-level needs.

func (*Conn) NextReader

func (c *Conn) NextReader() (MessageType, io.Reader, error)

NextReader returns a reader for the next data message. Only one reader may be active at a time; opening a new one discards any unread bytes of the previous message. A compressed message is fully inflated and returned as an in-memory reader; an uncompressed one is streamed.

func (*Conn) NextWriter

func (c *Conn) NextWriter(mt MessageType) (io.WriteCloser, error)

NextWriter returns a writer for the next message of the given data type. The message is sent when the writer is closed. Only one writer may be open at a time.

func (*Conn) ReadJSON

func (c *Conn) ReadJSON(v any) error

ReadJSON reads the next data message and unmarshals it into v. Control frames are handled transparently by the underlying read.

func (*Conn) ReadMessage

func (c *Conn) ReadMessage() (MessageType, []byte, error)

ReadMessage reads the next complete data message. It transparently handles interleaved control frames (answering pings, tracking pongs and closes) and, if negotiated, decompresses the message. A text message is validated as UTF-8.

func (*Conn) RemoteAddr

func (c *Conn) RemoteAddr() net.Addr

RemoteAddr returns the remote network address.

func (*Conn) SetCloseHandler

func (c *Conn) SetCloseHandler(fn func(code CloseCode, text string) error)

SetCloseHandler sets the handler for received close frames. The default sends a close frame back. A custom handler replaces that behaviour.

func (*Conn) SetPingHandler

func (c *Conn) SetPingHandler(fn func(appData string) error)

SetPingHandler sets the handler for received ping frames. The default sends a pong with the same payload. Setting a handler disables the automatic pong, so a custom handler that wants to reply must do so itself.

func (*Conn) SetPongHandler

func (c *Conn) SetPongHandler(fn func(appData string) error)

SetPongHandler sets the handler for received pong frames. The default ignores them.

func (*Conn) SetReadDeadline

func (c *Conn) SetReadDeadline(t time.Time) error

SetReadDeadline sets the deadline for future reads. A zero value clears it.

func (*Conn) SetReadLimit

func (c *Conn) SetReadLimit(n int64)

SetReadLimit sets the maximum size in bytes of a single received message. A message that would exceed it fails the read and closes the connection with 1009. A value <= 0 restores the default.

func (*Conn) SetWriteDeadline

func (c *Conn) SetWriteDeadline(t time.Time) error

SetWriteDeadline sets the deadline for future writes and for a write already in progress, as on a net.Conn. A zero value clears it. The deadline is remembered so that the connection's own control writes (auto-pong, close echo) can restore it instead of leaving a stale deadline.

It does not wait for the write lock: a write that is stuck on a peer that stopped reading is exactly what a deadline is for.

func (*Conn) SetWriteLimit added in v0.3.0

func (c *Conn) SetWriteLimit(n int64)

SetWriteLimit sets the maximum size in bytes of a single outgoing message. A write past it fails with ErrWriteLimit and nothing is sent; the connection stays usable. A value <= 0, the default, means no bound.

It exists because a message is built in memory before any of it goes out: Conn.NextWriter buffers everything written to it until Close, so io.Copy from an unbounded source is bounded by nothing but available memory. A limit turns that into an error the application can handle.

Like SetReadLimit it is not synchronized with writes in progress: set it before the connection is handed to the code that writes.

func (*Conn) Subprotocol

func (c *Conn) Subprotocol() string

Subprotocol returns the negotiated subprotocol, or an empty string if none was selected.

func (*Conn) WriteControl

func (c *Conn) WriteControl(mt MessageType, data []byte, deadline time.Time) error

WriteControl sends a control frame (close, ping or pong). The deadline bounds the whole call, including the wait for a data write in progress to finish; when it passes first the frame is not sent and the error reports a timeout. A zero deadline means no timeout. WriteControl may be called from a goroutine other than the one writing messages.

func (*Conn) WriteJSON

func (c *Conn) WriteJSON(v any) error

WriteJSON marshals v to JSON and sends it as a text message.

func (*Conn) WriteMessage

func (c *Conn) WriteMessage(mt MessageType, data []byte) error

WriteMessage sends data as a single, unfragmented message of the given type. If write compression is enabled, a data message is compressed with permessage-deflate. For control types it is equivalent to WriteControl with the write deadline already set on the connection.

type DialOption

type DialOption func(*dialConfig)

DialOption configures a client Dial.

func WithDialCompression

func WithDialCompression() DialOption

WithDialCompression offers the permessage-deflate extension to the server.

func WithDialCompressionLevel added in v0.6.0

func WithDialCompressionLevel(level int) DialOption

WithDialCompressionLevel sets the flate level used for outgoing messages, the client-side counterpart of WithCompressionLevel.

func WithDialHandshakeLimit added in v0.2.0

func WithDialHandshakeLimit(bytes int64) DialOption

WithDialHandshakeLimit bounds how many bytes of the server's handshake response Dial reads before failing with ErrHandshakeTooLarge. The default is 64 KiB; a value <= 0 removes the bound. It applies only to the handshake, so it does not limit messages: use Conn.SetReadLimit for those.

func WithDialHandshakeTimeout

func WithDialHandshakeTimeout(d time.Duration) DialOption

WithDialHandshakeTimeout bounds the time spent establishing the connection once the TCP dial has succeeded: the TLS handshake for wss, writing the request and reading the response. The default is 45s; a value <= 0 leaves the context as the only bound.

When the context carries a deadline of its own, the earlier of the two applies, so a long-lived context cannot quietly replace a short timeout.

func WithDialHeader

func WithDialHeader(h http.Header) DialOption

WithDialHeader adds extra HTTP headers to the client handshake request (for example Authorization or Cookie). The header is cloned, so the caller may go on using and changing its own copy.

func WithDialNetDialer

func WithDialNetDialer(d *net.Dialer) DialOption

WithDialNetDialer sets the net.Dialer used to establish the TCP connection.

func WithDialReadLimit added in v0.6.0

func WithDialReadLimit(bytes int64) DialOption

WithDialReadLimit sets the maximum size of a received message on the connection Dial returns, the client-side counterpart of WithReadLimit.

func WithDialSubprotocols

func WithDialSubprotocols(names ...string) DialOption

WithDialSubprotocols sets the subprotocols the client offers. Each name must be an HTTP token, and the slice is copied.

func WithDialTLSConfig

func WithDialTLSConfig(cfg *tls.Config) DialOption

WithDialTLSConfig sets the TLS configuration used for wss URLs. It is cloned on the way in, so a later change by the caller cannot alter a dial that is already under way.

type HandshakeError

type HandshakeError struct {
	Status int
	// contains filtered or unexported fields
}

HandshakeError describes a failed WebSocket upgrade. Upgrade returns it as a *HandshakeError, so errors.As(err, &he) with a *HandshakeError finds it, and it has already written an HTTP error response to the client by then.

Status is the HTTP status that was written, which classifies the failure without reading the message: 405 for the wrong method, 400 for a malformed request, 426 for the wrong version, 403 for a rejected origin and 500 for a response writer that cannot be hijacked.

func (*HandshakeError) Error

func (e *HandshakeError) Error() string

Error implements the error interface, returning the reason the upgrade failed.

type MessageType

type MessageType int

MessageType identifies a WebSocket frame opcode as defined in RFC 6455 section 5.2. The data types TextMessage and BinaryMessage are what an application normally sends and receives; the control types are handled by the connection but are also accepted by WriteControl.

const (
	// TextMessage denotes a UTF-8 encoded text message.
	TextMessage MessageType = 1
	// BinaryMessage denotes a binary message.
	BinaryMessage MessageType = 2
	// CloseMessage denotes a close control message.
	CloseMessage MessageType = 8
	// PingMessage denotes a ping control message.
	PingMessage MessageType = 9
	// PongMessage denotes a pong control message.
	PongMessage MessageType = 10
)

type Option

type Option func(*Upgrader)

Option configures an Upgrader (server side).

func WithCompression

func WithCompression() Option

WithCompression enables the permessage-deflate extension when the client offers it.

func WithCompressionLevel

func WithCompressionLevel(level int) Option

WithCompressionLevel sets the flate level used for outgoing messages (flate.HuffmanOnly to flate.BestCompression). A level outside that range fails the upgrade with ErrConfig rather than being silently replaced by the default, which used to hide a typo behind working compression.

func WithHandshakeTimeout

func WithHandshakeTimeout(d time.Duration) Option

WithHandshakeTimeout bounds the time spent writing the upgrade response.

func WithOriginChecker

func WithOriginChecker(fn func(*http.Request) bool) Option

WithOriginChecker sets the function that decides whether a request's Origin is allowed. The default accepts only same-origin requests, which guards against cross-site WebSocket hijacking. To allow any origin (for example a public API or local development) pass a function that always returns true.

func WithReadLimit

func WithReadLimit(bytes int64) Option

WithReadLimit sets the maximum size of a single received message.

func WithSubprotocols

func WithSubprotocols(names ...string) Option

WithSubprotocols sets the subprotocols the server supports, in order of preference. The first one the client also offers is selected.

Each name must be an HTTP token, since it is sent in a header; anything else fails the upgrade with ErrConfig instead of putting a malformed header on the wire. The slice is copied, so the caller may reuse it.

type Upgrader

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

Upgrader holds a reusable server-side upgrade configuration.

func NewUpgrader

func NewUpgrader(opts ...Option) *Upgrader

NewUpgrader returns an Upgrader configured with the given options. By default it accepts only same-origin requests and does not enable compression.

func (*Upgrader) Upgrade

func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request) (*Conn, error)

Upgrade completes the WebSocket handshake and returns a Conn. On failure it writes an HTTP error response and returns a *HandshakeError (or the hijack error).

Jump to

Keyboard shortcuts

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