engineio

package module
v0.0.0-...-9624d6b Latest Latest
Warning

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

Go to latest
Published: Jun 9, 2026 License: MIT Imports: 22 Imported by: 0

README

go-engine.io

Build Workflow codecov Pkg Go Dev

A Go implementation of the Engine.IO v4 protocol: the transport layer that Socket.IO is built on. Engine.IO provides a reliable, bidirectional connection between a client and a server using HTTP long-polling and WebSocket, with an automatic upgrade from long-polling to WebSocket once a session is established. This package ships a client (Socket), a server (Server, an http.Handler), and a version-aware packet/payload codec.

Features

  • Client: A Socket that performs the handshake, runs the heartbeat, and buffers writes across an upgrade
  • Server: A Server that is a plain http.Handler, mountable in any router
  • HTTP Long-Polling Transport: The baseline transport that works everywhere
  • WebSocket Transport: A full-duplex transport for low-latency messaging
  • WebTransport Transport: An HTTP/3 transport for client and server, built on quic-go
  • Automatic Transport Upgrade: Background probe that upgrades long-polling to WebSocket or WebTransport
  • Binary Support: Round-trip binary messages without downgrading them to text
  • Version-Aware Decoding: Decodes v2, v3, and v4 long-polling payload framings
  • Heartbeat: Server-initiated v4 ping/pong with timeout detection
  • CORS: Configurable cross-origin policy on the server
  • Concurrent Safe: All client and server socket methods are safe for concurrent use
  • Standard Library HTTP: Built on net/http; works with Echo, Gin, chi, and more

Resources

Installation

go get github.com/lewisgibson/go-engine.io

Quickstart

Client

Create a Socket, register handlers, then Open it. Sends made after Open() but before the handshake completes are buffered and flushed once the socket opens; a Send before Open() (or after close) is silently dropped. All options are optional; NewSocket(url) works out of the box with the defaults in the table below.

package main

import (
	"context"
	"fmt"

	engineio "github.com/lewisgibson/go-engine.io"
)

func main() {
	ctx := context.Background()

	// NewSocket(url) works out of the box. Every option is optional (see the table
	// below); by default the socket starts on long-polling and upgrades to
	// WebSocket.
	client, err := engineio.NewSocket("http://localhost:3000/engine.io/")
	if err != nil {
		panic(err)
	}

	// Invoked once the handshake completes. You need not wait for it to send -- a
	// Send any time after Open() is buffered and flushed here.
	client.OnOpen(func() {
		if err := client.Send(ctx, []engineio.Packet{
			{Type: engineio.PacketMessage, Data: []byte("Hello")},
		}); err != nil {
			fmt.Printf("send error: %v\n", err)
		}
	})

	// Invoked for each application message. isBinary reports whether the peer
	// sent it as a binary frame.
	client.OnMessage(func(data []byte, isBinary bool) {
		fmt.Printf("message (binary=%t): %s\n", isBinary, string(data))
	})

	// Invoked for every packet, including protocol packets such as ping and open.
	client.OnPacket(func(packet engineio.Packet) {
		fmt.Printf("packet: %s\n", packet)
	})

	// Invoked once the socket finishes upgrading to a better transport.
	client.OnUpgrade(func(transportType engineio.TransportType) {
		fmt.Printf("upgraded to: %s\n", transportType)
	})

	// Invoked on a transport error; an error is not always fatal.
	client.OnError(func(err error) {
		fmt.Printf("error: %v\n", err)
	})

	// Invoked once when the socket closes. cause is the error if one occurred,
	// else nil (a clean close and a ping timeout both pass nil; branch on reason).
	client.OnClose(func(reason string, cause error) {
		fmt.Printf("close: %s (%v)\n", reason, cause)
	})

	// Open the connection, then close it when done.
	client.Open(ctx)
	defer client.Close(ctx)
}

The client handlers are:

Handler Fires when
OnOpen(func()) The handshake completes and the socket is ready
OnMessage(func(data []byte, isBinary bool)) An application message arrives
OnPacket(func(Packet)) Any packet arrives, including protocol packets
OnUpgrade(func(TransportType)) The socket switches to a better transport
OnUpgradeError(func(error)) An upgrade probe fails (non-fatal; stays on the current transport)
OnError(func(error)) A transport error occurs (not always fatal); also receives upgrade-probe failures when no OnUpgradeError handler is set
OnClose(func(reason string, cause error)) The socket closes

The client options, with their defaults:

Option Default Purpose
WithClient(TransportClient) &http.Client{} HTTP client used by the transports
WithHeader(http.Header) empty http.Header Headers sent on every transport request
WithUpgrade(bool) true Try to upgrade long-polling to a better transport
WithRememberUpgrade(bool) false Reuse a prior successful upgrade on the next open
WithTransports(...TransportType) polling, then websocket Transports to try, in order
WithTryAllTransports(bool) false Try every transport in the list before giving up

See examples/client for a complete program.

Server

The server is an http.Handler. Mount it at the Engine.IO path (typically /engine.io/) and configure each session in the connection handler.

All server options are optional; NewServer() works out of the box with the defaults in the table below. Configure only what you need.

package main

import (
	"fmt"
	"net/http"
	"time"

	engineio "github.com/lewisgibson/go-engine.io"
)

func main() {
	// Every option is optional. This passes one or two for illustration; pass
	// none to accept the defaults.
	server := engineio.NewServer(
		engineio.WithPingInterval(20 * time.Second),
		engineio.WithCORS(engineio.CORSOptions{
			AllowedOrigins: []string{"https://example.com"},
		}),
	)

	server.OnConnection(func(socket *engineio.ServerSocket) {
		fmt.Printf("connected: %s\n", socket.ID())

		// Echo every message back to its sender, preserving the binary flag.
		socket.OnMessage(func(data []byte, isBinary bool) {
			if err := socket.Send(data, isBinary); err != nil {
				fmt.Printf("send error: %v\n", err)
			}
		})

		// cause is the error if one occurred, else nil (branch on reason, not cause).
		socket.OnClose(func(reason string, cause error) {
			fmt.Printf("disconnected: %s (%s)\n", socket.ID(), reason)
		})
	})

	mux := http.NewServeMux()
	mux.Handle("/engine.io/", server)

	httpServer := &http.Server{
		Addr:              ":3000",
		Handler:           mux,
		ReadHeaderTimeout: 10 * time.Second,
	}
	if err := httpServer.ListenAndServe(); err != nil {
		fmt.Printf("server error: %v\n", err)
	}
}

ServerSocket is the per-session handle passed to the connection handler:

  • OnMessage(func(data []byte, isBinary bool)) registers the message handler.
  • OnClose(func(reason string, cause error)) registers the close handler.
  • Send(data []byte, isBinary bool) error queues a message; pass true to send it as binary. It returns ErrSocketClosed once the session is closing or closed -- from the moment Close() (or a teardown) begins, which may be before the close handler fires.
  • ID() string returns the session identifier from the handshake.
  • Close() error gracefully closes the session, delivering a close packet first.

Server.Close() tears down every live session with reason "forced close", firing each socket's close handler; it does not stop the underlying http.Server, which the caller shuts down separately.

For fan-out, the server exposes its live sessions directly, so an application never has to keep its own registry:

  • Sockets() []*ServerSocket returns a snapshot of every live session, safe to range over and send on. It is the building block for broadcast, since a ServerSocket only ever talks to its own client.
  • Count() int returns the number of live sessions.
  • Socket(id string) (*ServerSocket, bool) looks a session up by its id; the bool reports whether a live session with that id exists.

These mirror the reference engine.io server's clients registry and clientsCount. See examples/broadcast for a chat-style relay built on Sockets().

The server options, with their defaults:

Option Default Purpose
WithPingInterval(time.Duration) DefaultPingInterval (25s) How often the server pings
WithPingTimeout(time.Duration) DefaultPingTimeout (20s) How long to wait for a pong before closing
WithUpgradeTimeout(time.Duration) DefaultUpgradeTimeout (10s) How long an upgrade probe may take
WithMaxPayload(int) DefaultMaxPayload (1000000) Maximum accepted POST body size, in bytes
WithServerTransports(...TransportType) polling, websocket Transports the server accepts
WithAllowUpgrades(bool) true Advertise and accept transport upgrades
WithCORS(CORSOptions) allow all origins Cross-origin policy (see below)
WithGenerateID(func(r *http.Request) string) 18 random bytes, base64url-encoded Session identifier generator; receives the handshake request, so the id can be derived from a header or token
WithAllowRequest(func(r *http.Request) error) unset (allow all) Gate each handshake (auth, tokens, rate limit); a non-nil error rejects it with 403
WithCookie(CookieOptions) no cookie Session-affinity cookie for sticky sessions behind a load balancer
WithHTTPCompression(bool) true gzip long-poll responses above a threshold when the client advertises it

The server also exposes OnConnectionError(func(r *http.Request, code ConnectionErrorCode, reason string)), invoked when a handshake is rejected by validation before a session is established (a failed allow-request gate, an unsupported protocol version, an unknown or disallowed transport, a bad handshake method, or an unknown polling session id). It is best-effort: a few low-level failures bypass it (an unknown-sid WebSocket upgrade, or a failure to build or send the open packet). code is one of the exported ConnectionError* constants (ConnectionErrorUnknownTransport, ConnectionErrorUnknownSessionID, ConnectionErrorBadHandshakeMethod, ConnectionErrorBadRequest, ConnectionErrorForbidden, ConnectionErrorUnsupportedProtocolVersion), so a handler can branch on why the connection was refused.

CORSOptions controls the cross-origin headers the server sets:

type CORSOptions struct {
	// AllowCredentials sets Access-Control-Allow-Credentials. When true, the
	// server echoes the request origin instead of replying with "*".
	AllowCredentials bool
	// AllowedOrigins is the set of origins permitted to connect. An empty slice,
	// or a single "*" entry, allows every origin.
	AllowedOrigins []string
	// AllowedHeaders is advertised in the preflight response. An empty slice
	// advertises Content-Type.
	AllowedHeaders []string
}

See examples/server for a complete program with graceful shutdown.

Web frameworks

Because the server is a plain http.Handler, it mounts in any router. Complete examples live in examples/:

// net/http
mux.Handle("/engine.io/", server)

// Echo (github.com/labstack/echo/v4)
e.Any("/engine.io/*", echo.WrapHandler(server))

// Gin (github.com/gin-gonic/gin)
r.Any("/engine.io/*any", gin.WrapH(server))

// chi (github.com/go-chi/chi/v5)
r.Handle("/engine.io/*", server)

Transports

Engine.IO defines three transports, all implemented here:

  • HTTP long-polling (TransportTypePolling): the baseline. The client issues a long-lived GET that the server holds open until it has data to deliver, and posts outbound packets with POST. It works in every environment, including behind restrictive proxies, but carries the latency of repeated requests.
  • WebSocket (TransportTypeWebSocket): a full-duplex connection that carries packets as individual frames with no per-message HTTP overhead, including native binary frames.
  • WebTransport (TransportTypeWebTransport): a full-duplex connection over HTTP/3 (QUIC). It carries each packet as a length-framed message on a single bidirectional stream, with native binary support. Because it runs over UDP and always uses TLS, it needs its own HTTP/3 listener alongside the TCP one.

A session starts on long-polling and, with upgrades enabled, the client probes for a better transport in the background: it opens a second transport, sends a ping with the "probe" payload, and on the matching pong commits the switch with an upgrade packet. Writes are buffered across the switch so nothing is lost or reordered. Upgrades are on by default; disable them with WithUpgrade(false) on the client or WithAllowUpgrades(false) on the server.

WebTransport

WebTransport is opt-in. It pulls in quic-go and webtransport-go, and it needs an HTTP/3 listener you create and run yourself. The same Server handler serves polling and WebSocket over TCP and WebTransport over UDP.

On the server, build a *webtransport.Server, pass it with WithWebTransportServer, list TransportTypeWebTransport among the accepted transports, and run the HTTP/3 listener on the same handler the TCP server uses. WithWebTransportServer configures the HTTP/3 server for WebTransport for you (it calls webtransport.ConfigureHTTP3Server), so you do not have to:

import (
	"github.com/quic-go/quic-go/http3"
	"github.com/quic-go/webtransport-go"
)

mux := http.NewServeMux()
wt := &webtransport.Server{
	H3: &http3.Server{Addr: ":443", Handler: mux, EnableDatagrams: true},
	// A nil CheckOrigin only permits same-origin requests; screen your own origins
	// here (this is where cross-origin browser sessions are allowed or rejected).
	CheckOrigin: func(r *http.Request) bool { return true },
}

server := engineio.NewServer(
	engineio.WithServerTransports(
		engineio.TransportTypePolling,
		engineio.TransportTypeWebSocket,
		engineio.TransportTypeWebTransport,
	),
	engineio.WithWebTransportServer(wt),
)
mux.Handle("/engine.io/", server)

go http.ListenAndServeTLS(":443", "cert.pem", "key.pem", mux) // polling + websocket (TCP)
go wt.ListenAndServeTLS("cert.pem", "key.pem")                // webtransport (UDP / HTTP3)

On the client, supply a *webtransport.Dialer (which carries the TLS and QUIC configuration) with WithWebTransportDialer, and list WebTransport among the transports to try:

client, err := engineio.NewSocket("https://example.com:443/engine.io/",
	engineio.WithTransports(
		engineio.TransportTypePolling,
		engineio.TransportTypeWebTransport,
	),
	engineio.WithWebTransportDialer(&webtransport.Dialer{}),
)

A few notes:

  • WebTransport is advertised as an upgrade target only when WithWebTransportServer is set, so a misconfigured server never sends clients probing a transport it cannot serve.
  • The allow-request gate (WithAllowRequest) runs for a fresh WebTransport session but not for an upgrade, matching the other transports. Because WebTransport bypasses the HTTP layer, cross-origin requests are screened by the *webtransport.Server's CheckOrigin, not by WithCORS.
  • A client URL may use http:// or https://; the WebTransport transport always dials over https://.

Compatibility

This library always speaks Engine.IO protocol v4 on the wire (the Protocol constant, sent as the EIO query parameter). Payload decoding is version-aware so the codec can read messages produced by older peers, but encoding is v4-only.

Concern v2 (ProtocolVersion2) v3 (ProtocolVersion3) v4 (ProtocolVersion4)
Payload decoding ✅ string framing ✅ string + binary framing ✅ record-separator framing
Payload encoding
Server (EIO) ✅ only
Client (EIO) ✅ only

The server rejects any handshake whose EIO is not 4; the client always sends EIO=4.

Interoperability with the reference JavaScript implementation is verified by the build-tagged tests in test/interop: the Go client is driven against the canonical engine.io server, and the Go server against the canonical engine.io-client, exercising the handshake, long-polling, the WebSocket upgrade, binary messages, and the heartbeat. WebTransport is verified separately, by Go-to-Go round-trip tests over a real QUIC connection, and its round trip has additionally been confirmed against a real browser (Chrome's native WebTransport); it is not part of this Node.js suite because the only Node.js WebTransport implementation does not interoperate with quic-go at the QUIC layer. Run the suite with Node.js installed:

make interop

Codec

The packet and payload codec is exported for direct use. A Packet is a single protocol packet; a payload is a long-polling framing of one or more packets.

A Packet carries a type, its data, and a flag marking binary messages:

type Packet struct {
	// Type is the packet kind (open, close, ping, pong, message, upgrade, noop).
	Type PacketType
	// Data is the payload: UTF-8 bytes for a text packet, raw bytes for a binary
	// message (already base64-decoded).
	Data []byte
	// IsBinary reports whether Data is a binary message. Only a message may be
	// binary, so IsBinary == true implies Type == PacketMessage.
	IsBinary bool
}

The seven packet types are PacketOpen, PacketClose, PacketPing, PacketPong, PacketMessage, PacketUpgrade, and PacketNoop.

Encode and decode a single packet:

packet := engineio.Packet{Type: engineio.PacketMessage, Data: []byte("hello")}

// EncodePacket returns the text wire form ("4hello").
encoded := engineio.EncodePacket(packet)

// DecodePacket parses it back into a Packet.
decoded, err := engineio.DecodePacket(encoded)
if err != nil {
	panic(err)
}
fmt.Printf("%s\n", decoded) // Packet{Type: message, Data: hello}

Encode and decode a long-polling payload of several packets. Encoding is v4-only; decoding takes the negotiated ProtocolVersion so it can read v2/v3 framings too:

packets := []engineio.Packet{
	{Type: engineio.PacketMessage, Data: []byte("hello")},
	{Type: engineio.PacketMessage, Data: []byte("world")},
}

// EncodePayload joins the packets with the v4 record separator.
body := engineio.EncodePayload(packets)

// DecodePayload frames the body back into packets for the given version.
decoded, err := engineio.DecodePayload(engineio.ProtocolVersion4, body)
if err != nil {
	panic(err)
}
fmt.Printf("decoded %d packets\n", len(decoded))

API Reference

Core Types
  • Socket - the Engine.IO v4 client connection; created with NewSocket.
  • Server - the Engine.IO v4 server (http.Handler); created with NewServer.
  • ServerSocket - a single connected server session, handed to OnConnection.
  • Transport / TransportType - the transport interface and its kinds (TransportTypePolling, TransportTypeWebSocket, TransportTypeWebTransport).
  • Packet / PacketType - a protocol packet and its type constants.
  • OpenPacket - the JSON payload of the handshake open packet.
  • CORSOptions - the server's cross-origin configuration.
  • ProtocolVersion - an Engine.IO version (ProtocolVersion2/3/4); Protocol is the version this library speaks (v4).
Constructors
  • NewSocket(serverURL string, options ...SocketOption) (*Socket, error) - build a client.
  • NewServer(options ...ServerOption) *Server - build a server.
Codec Functions
  • EncodePacket(packet Packet) []byte - encode one packet to its text wire form.
  • DecodePacket(input []byte) (Packet, error) - decode one packet.
  • EncodePayload(packets []Packet) []byte - encode packets to a v4 payload.
  • DecodePayload(version ProtocolVersion, input []byte) ([]Packet, error) - decode a payload for the given protocol version (v2/v3/v4).
Sentinel Errors
  • ErrInvalidURL - the server URL could not be parsed (NewSocket).
  • ErrNoTransports - no transports are available to open (Socket.Open).
  • ErrSocketClosed - a send was attempted on a closed ServerSocket.
  • ErrEmptyPacket - the codec was given empty input.
  • ErrInvalidPacketType - the codec saw an unknown packet type byte.
  • ErrMalformedPayload - a payload could not be framed.
  • ErrUnsupportedProtocolVersion - decoding was requested for a version the codec does not understand.
  • ErrURLRequired - a transport constructor was called with a nil URL.
  • ErrWebTransportDialerRequired - a WebTransport transport was constructed without a *webtransport.Dialer.
  • ErrTransportRoundTripperClientRequired - a TransportRoundTripper was used without a Client.
  • ErrUnexpectedStatus - a polling transport request returned a non-200 HTTP status.

Performance

The codec, the server send path, and the version-aware decoders have benchmarks. Run them with:

go test -run '^$' -bench . -benchmem .

Indicative results on an AMD Ryzen 9 9950X3D (linux/amd64, Go 1.26); your numbers will differ:

Benchmark ns/op B/op allocs/op
EncodePacket (small text) 23 24 2
DecodePacket (small text) 77 0 0
EncodePacket (binary) 203 360 2
DecodePacket (binary) 323 640 2
EncodePayload (small) 45 56 3
EncodePayload (large batch) 698 1912 8
DecodePayload (v4 record separator) 216 264 3
DecodePayload (v3 binary framing) 154 136 3
DecodePayload (v2 string framing) 203 144 4
Server.Send (text) 7,900 7209 33
Server poll round trip 9,900 7208 33

Examples

The examples directory has complete, runnable programs (see its README for the full index), including:

License

This project is licensed under the MIT License - see the LICENSE.md file for details.

Documentation

Overview

Package engineio implements the Engine.IO v4 protocol: a client, a server, and a version-aware packet/payload codec.

https://github.com/socketio/engine.io-protocol

Index

Examples

Constants

View Source
const (
	// DefaultPingInterval is how often the server sends a ping.
	DefaultPingInterval = 25 * time.Second
	// DefaultPingTimeout is how long the server waits for a pong before closing.
	DefaultPingTimeout = 20 * time.Second
	// DefaultUpgradeTimeout is how long a transport upgrade probe may take.
	DefaultUpgradeTimeout = 10 * time.Second
	// DefaultMaxPayload is the maximum accepted POST body size, in bytes. It also
	// bounds a single inbound WebTransport frame.
	DefaultMaxPayload = 1_000_000
)

Default server options.

View Source
const BinaryMarker = 'b'

BinaryMarker is the byte that prefixes a base64-encoded binary packet.

https://github.com/socketio/engine.io-protocol#packet-encoding

View Source
const Protocol = ProtocolVersion4

Protocol is the protocol version this library implements.

View Source
const (
	// Separator joins packets in a v4 long-polling payload (record separator).
	//
	// https://github.com/socketio/engine.io-protocol#http-long-polling-1
	Separator = '\x1e'
)

Variables

View Source
var (
	ErrEmptyPacket       = errors.New("empty packet")
	ErrInvalidPacketType = errors.New("invalid packet type")
)

Sentinel Errors.

View Source
var (
	ErrMalformedPayload           = errors.New("malformed payload")
	ErrUnsupportedProtocolVersion = errors.New("unsupported protocol version")
)

Sentinel Errors.

View Source
var (
	ErrInvalidURL   = errors.New("invalid URL")
	ErrNoTransports = errors.New("no transports available")
)

Sentinel Errors.

View Source
var (
	ErrTransportRoundTripperClientRequired = errors.New("transport round tripper client is required")
	ErrURLRequired                         = errors.New("url is required")
	ErrUnexpectedStatus                    = errors.New("unexpected HTTP status")
)

Sentinel Errors.

View Source
var (
	ErrSocketClosed = errors.New("socket is closed")
)

Sentinel Errors.

View Source
var (
	ErrWebTransportDialerRequired = errors.New("webtransport dialer is required")
)

Sentinel Errors.

Transports maps each transport type to the constructor that builds it. A Socket snapshots this registry at construction, so replacing an entry affects only sockets created afterwards; this is the seam for injecting a custom or mock transport in tests.

Functions

func EncodePacket

func EncodePacket(packet Packet) []byte

EncodePacket encodes a packet into its text wire form. A binary message is base64-encoded behind the BinaryMarker; any other packet is its type byte followed by its data. Binary messages sent over a transport with native binary frames (WebSocket) are written as raw frames and never pass here.

Example

ExampleEncodePacket round-trips a single packet through the text wire codec.

package main

import (
	"fmt"

	engineio "github.com/lewisgibson/go-engine.io"
)

func main() {
	packet := engineio.Packet{Type: engineio.PacketMessage, Data: []byte("hello")}

	encoded := engineio.EncodePacket(packet)

	decoded, err := engineio.DecodePacket(encoded)
	if err != nil {
		panic(err)
	}

	fmt.Printf("%s -> %s\n", encoded, decoded)
}
Output:
4hello -> Packet{Type: message, Data: hello}

func EncodePayload

func EncodePayload(packets []Packet) []byte

EncodePayload encodes packets into a v4 long-polling payload. Encoding is v4-only: this library always speaks v4, so it never needs to produce the legacy v2/v3 framings (it only decodes them, via DecodePayload).

Example

ExampleEncodePayload round-trips a long-polling payload of several packets. Encoding is v4-only; decoding takes the negotiated protocol version.

package main

import (
	"fmt"

	engineio "github.com/lewisgibson/go-engine.io"
)

func main() {
	packets := []engineio.Packet{
		{Type: engineio.PacketMessage, Data: []byte("hello")},
		{Type: engineio.PacketMessage, Data: []byte("world")},
	}

	body := engineio.EncodePayload(packets)

	decoded, err := engineio.DecodePayload(engineio.ProtocolVersion4, body)
	if err != nil {
		panic(err)
	}

	fmt.Printf("decoded %d packets\n", len(decoded))
}
Output:
decoded 2 packets

Types

type CORSOptions

type CORSOptions struct {
	// AllowCredentials sets Access-Control-Allow-Credentials. When true the
	// server echoes the request origin rather than replying with "*".
	AllowCredentials bool
	// AllowedOrigins is the set of origins permitted to connect. An empty slice
	// allows every origin. A single "*" entry also allows every origin.
	AllowedOrigins []string
	// AllowedHeaders is the set of request headers advertised in the preflight
	// response. An empty slice advertises Content-Type.
	AllowedHeaders []string
}

CORSOptions configures the cross-origin headers the server sets. An empty AllowedOrigins allows every origin.

type ConnectionErrorCode

type ConnectionErrorCode int

ConnectionErrorCode is an Engine.IO connection error code. It is sent in the JSON error body of a rejected handshake or polling request and passed to a ServerConnectionErrorHandler, so callers can branch on why a connection was refused.

https://github.com/socketio/engine.io/blob/main/packages/engine.io/lib/server.ts

const (
	// ConnectionErrorUnknownTransport is sent when the requested transport is not enabled.
	ConnectionErrorUnknownTransport ConnectionErrorCode = 0
	// ConnectionErrorUnknownSessionID is sent when the session identifier is not recognised.
	ConnectionErrorUnknownSessionID ConnectionErrorCode = 1
	// ConnectionErrorBadHandshakeMethod is sent when a handshake uses the wrong HTTP method.
	ConnectionErrorBadHandshakeMethod ConnectionErrorCode = 2
	// ConnectionErrorBadRequest is sent when a request is otherwise malformed.
	ConnectionErrorBadRequest ConnectionErrorCode = 3
	// ConnectionErrorForbidden is sent when a request is rejected by the origin policy.
	ConnectionErrorForbidden ConnectionErrorCode = 4
	// ConnectionErrorUnsupportedProtocolVersion is sent when EIO is not 4.
	ConnectionErrorUnsupportedProtocolVersion ConnectionErrorCode = 5
)

type CookieOptions

type CookieOptions struct {
	// Name is the cookie name. Default "io".
	Name string
	// Path is the cookie path. Default "/".
	Path string
	// HTTPOnly sets the HttpOnly attribute. Recommended true.
	HTTPOnly bool
	// Secure sets the Secure attribute (cookie sent over HTTPS only).
	Secure bool
	// SameSite sets the SameSite attribute. Default http.SameSiteLaxMode.
	SameSite http.SameSite
	// MaxAge sets Max-Age in seconds. Zero leaves it unset (a session cookie).
	MaxAge int
}

CookieOptions configures the session-affinity cookie set on the handshake response. It is used for sticky sessions behind a load balancer that routes by cookie, so a client's long-polling and upgrade requests reach the same node.

type OpenPacket

type OpenPacket struct {
	// SessionID is the unique identifier the server assigns to this connection.
	// The client echoes it as the "sid" query parameter on every later request.
	SessionID string `json:"sid"`

	// Upgrades lists the transports the server is willing to upgrade to. An empty
	// list means the client must stay on its current transport.
	//
	// https://github.com/socketio/engine.io-protocol?tab=readme-ov-file#upgrade
	Upgrades []TransportType `json:"upgrades"`

	// PingInterval is how often, in milliseconds, the server sends a ping; the
	// client uses it (with PingTimeout) to detect a silent connection.
	//
	// https://github.com/socketio/engine.io-protocol?tab=readme-ov-file#heartbeat
	PingInterval int `json:"pingInterval"`

	// PingTimeout is how long, in milliseconds, to wait for server activity before
	// treating the connection as dead and closing it.
	//
	// https://github.com/socketio/engine.io-protocol?tab=readme-ov-file#heartbeat
	PingTimeout int `json:"pingTimeout"`

	// MaxPayload is the maximum number of bytes the server accepts in a single
	// long-polling payload; the client splits its writes into chunks no larger.
	//
	// https://github.com/socketio/engine.io-protocol?tab=readme-ov-file#packet-encoding
	MaxPayload int `json:"maxPayload"`
}

OpenPacket is the JSON payload of a PacketOpen. The server sends it to seal the handshake and tell the client the session parameters it must honour; the client unmarshals it to learn its session id, heartbeat timings, payload limit, and which transports it may upgrade to.

https://github.com/socketio/engine.io-protocol?tab=readme-ov-file#handshake

type Packet

type Packet struct {
	// Type is the packet's kind, which determines how transports and handlers
	// treat it.
	Type PacketType
	// Data is the packet payload. For a text packet it holds the UTF-8 bytes;
	// for a binary message it holds the raw (already base64-decoded) bytes.
	Data []byte
	// IsBinary reports whether Data is a binary message. Engine.IO can only
	// carry binary as a message, so IsBinary being true implies Type is
	// PacketMessage. It is set explicitly rather than inferred from Data so
	// that callers and transports never have to re-sniff the bytes.
	IsBinary bool
}

Packet is a single Engine.IO protocol packet: a type plus its payload. It is the unit every transport encodes, decodes, and dispatches, so the same value round-trips through long-polling, WebSocket, the codec, and the handlers.

func DecodePacket

func DecodePacket(input []byte) (Packet, error)

DecodePacket decodes a single packet from its text wire form. A leading BinaryMarker denotes a base64-encoded binary message; otherwise the first byte is the packet type and the remainder is the data.

func DecodePayload

func DecodePayload(version ProtocolVersion, input []byte) ([]Packet, error)

DecodePayload decodes a long-polling payload body into packets according to the negotiated protocol version:

  • v4 concatenates packets with the record separator (0x1e).
  • v3 uses either the length-prefixed string framing ("<len>:<packet>") or the binary framing (0x00/0x01 marker, value-byte length, 0xff), selected by the first byte.
  • v2 uses the string framing only; a binary frame marker is malformed.

func (Packet) String

func (p Packet) String() string

String returns a human-readable representation of the packet for logging and test output. Binary data is rendered as hex so it stays printable; text data is rendered as-is.

type PacketType

type PacketType uint8

PacketType identifies which of the seven Engine.IO packet kinds a Packet is. On the wire it is encoded as a single ASCII digit ('0'..'6'); see Byte and PacketTypeFromByte for the mapping.

https://github.com/socketio/engine.io-protocol?tab=readme-ov-file#protocol

const (
	// PacketOpen carries the handshake details (session id, ping timings, and
	// offered upgrades) and is the first packet the server sends on a new session.
	//
	// https://github.com/socketio/engine.io-protocol?tab=readme-ov-file#handshake
	PacketOpen PacketType = iota

	// PacketClose signals that the transport can be closed; either peer may send it
	// to tear the session down gracefully.
	PacketClose

	// PacketPing is sent by the server as part of the v4 server-initiated heartbeat;
	// the client must answer with a PacketPong to keep the session alive. During an
	// upgrade probe it is also sent by the client with the "probe" payload.
	//
	// https://github.com/socketio/engine.io-protocol?tab=readme-ov-file#heartbeat
	PacketPing

	// PacketPong answers a PacketPing in the heartbeat. During an upgrade probe the
	// server replies to the client's "probe" ping with a "probe" pong.
	//
	// https://github.com/socketio/engine.io-protocol?tab=readme-ov-file#heartbeat
	PacketPong

	// PacketMessage carries application data. It is the only packet that may hold a
	// binary payload, so a Packet with IsBinary set is always a PacketMessage.
	PacketMessage

	// PacketUpgrade commits a transport upgrade: the client sends it over the
	// probing transport once the probe pong is received, telling the server to
	// switch traffic to the new transport.
	//
	// https://github.com/socketio/engine.io-protocol#upgrade
	PacketUpgrade

	// PacketNoop is a no-op used to release a held long-poll so the client can
	// finish pausing the polling transport during an upgrade.
	//
	// https://github.com/socketio/engine.io-protocol#upgrade
	PacketNoop
)

func PacketTypeFromByte

func PacketTypeFromByte(b byte) PacketType

PacketTypeFromByte decodes the ASCII digit byte used on the wire into a PacketType. It is the inverse of Byte. The result is not validated here; callers that read untrusted input should check it with the protocol's validity rules before trusting the type.

func PacketTypeFromInt

func PacketTypeFromInt(u uint8) PacketType

PacketTypeFromInt converts a raw numeric value into a PacketType, for callers that already hold the type as an integer rather than as its ASCII wire byte.

func (PacketType) Byte

func (p PacketType) Byte() byte

Byte returns the single ASCII digit that encodes the packet type on the wire ('0' for PacketOpen through '6' for PacketNoop). It is the inverse of PacketTypeFromByte.

func (PacketType) String

func (p PacketType) String() string

String returns the lowercase protocol name of the packet type ("open", "ping", and so on), or "unknown" for an unrecognised value. It is intended for logging rather than wire encoding; use Byte for the wire form.

type PollingTransport

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

PollingTransport is a transport that uses the HTTP long-polling protocol. Long-poll GET requests carry a per-request cache-busting timestamp in the "t" query parameter, mirroring the engine.io-client default, so a caching intermediary cannot serve a stale poll body. This is layered on the server's Cache-Control: no-store response header rather than replacing it.

func (*PollingTransport) Close

func (t *PollingTransport) Close(ctx context.Context)

Close closes the transport by sending a close packet and waiting for any in-flight poll to finish.

func (*PollingTransport) OnClose

func (t *PollingTransport) OnClose(handler TransportCloseHandler)

OnClose sets the handler for when the transport closes.

func (*PollingTransport) OnError

func (t *PollingTransport) OnError(handler TransportErrorHandler)

OnError sets the handler for when the transport encounters an error.

func (*PollingTransport) OnOpen

func (t *PollingTransport) OnOpen(handler TransportOpenHandler)

OnOpen sets the handler for when the transport opens.

func (*PollingTransport) OnPacket

func (t *PollingTransport) OnPacket(handler TransportPacketHandler)

OnPacket sets the handler for when the transport receives packets.

func (*PollingTransport) Open

func (t *PollingTransport) Open(ctx context.Context)

Open opens the transport by issuing the first long-poll.

func (*PollingTransport) Pause

func (t *PollingTransport) Pause(_ context.Context)

Pause stops polling and waits for any in-flight poll to finish. It is used while a transport upgrade is being probed.

func (*PollingTransport) Send

func (t *PollingTransport) Send(ctx context.Context, packets []Packet) error

Send sends packets through the transport as a single POST request.

func (*PollingTransport) SetURL

func (t *PollingTransport) SetURL(url *url.URL)

SetURL sets the URL for the transport.

func (*PollingTransport) State

func (t *PollingTransport) State() TransportState

State returns the state of the transport.

func (*PollingTransport) Type

func (t *PollingTransport) Type() TransportType

Type returns the type of the transport.

type ProtocolVersion

type ProtocolVersion int

ProtocolVersion is an Engine.IO protocol version, sent as the EIO query parameter during the handshake.

const (
	// ProtocolVersion2 is Engine.IO protocol v2.
	ProtocolVersion2 ProtocolVersion = 2
	// ProtocolVersion3 is Engine.IO protocol v3.
	ProtocolVersion3 ProtocolVersion = 3
	// ProtocolVersion4 is Engine.IO protocol v4, the version this library speaks.
	ProtocolVersion4 ProtocolVersion = 4
)

type Server

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

Server is an Engine.IO v4 server. It is an http.Handler; mount it at the Engine.IO path (typically "/engine.io/"). The server is v4-only: it rejects any other protocol version.

func NewServer

func NewServer(options ...ServerOption) *Server

NewServer creates a Server, applying the options over the defaults (see the DefaultX constants). Register a connection handler with OnConnection and mount the returned Server as an http.Handler to start accepting sessions.

Example

ExampleNewServer builds an Engine.IO server that echoes every message back to its sender and mounts it on the standard library's http.ServeMux. The server is a plain http.Handler, so it mounts in any router.

package main

import (
	"fmt"
	"net/http"

	engineio "github.com/lewisgibson/go-engine.io"
)

func main() {
	server := engineio.NewServer(
		engineio.WithPingInterval(engineio.DefaultPingInterval),
		engineio.WithPingTimeout(engineio.DefaultPingTimeout),
		engineio.WithCORS(engineio.CORSOptions{
			AllowCredentials: true,
			AllowedOrigins:   []string{"https://example.com"},
		}),
	)

	server.OnConnection(func(socket *engineio.ServerSocket) {
		fmt.Printf("connected: %s\n", socket.ID())

		// Echo every message back, preserving the binary flag.
		socket.OnMessage(func(data []byte, isBinary bool) {
			if err := socket.Send(data, isBinary); err != nil {
				fmt.Printf("send error: %v\n", err)
			}
		})

		socket.OnClose(func(reason string, cause error) {
			fmt.Printf("disconnected: %s (%s)\n", socket.ID(), reason)
		})
	})

	mux := http.NewServeMux()
	mux.Handle("/engine.io/", server)

	// Mount mux on an http.Server and serve as usual; omitted here so the example
	// does not block.
	_ = &http.Server{Addr: ":3000", Handler: mux}
}

func (*Server) Close

func (s *Server) Close()

Close tears down every live session with reason "forced close", firing each socket's close handler. It does not stop the underlying http.Server; the caller shuts that down separately.

func (*Server) Count

func (s *Server) Count() int

Count returns the number of live sessions. It mirrors the reference engine.io server's clientsCount.

func (*Server) OnConnection

func (s *Server) OnConnection(handler ServerConnectionHandler)

OnConnection registers the handler invoked once per new session, after the open packet is sent. The application configures the socket here.

func (*Server) OnConnectionError

func (s *Server) OnConnectionError(handler ServerConnectionErrorHandler)

OnConnectionError registers the handler invoked when a connection is rejected before a session is established. It replaces any previously registered handler; passing nil clears it.

func (*Server) ServeHTTP

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements http.Handler. It applies CORS, routes a WebTransport Extended CONNECT to the HTTP/3 upgrader, then for the HTTP transports rejects any protocol version other than v4 and any disabled transport before dispatching to the polling or websocket handler. Errors are written as Engine.IO JSON error bodies.

func (*Server) Socket

func (s *Server) Socket(id string) (*ServerSocket, bool)

Socket returns the socket for the given session identifier, reporting whether a live session with that id exists. It mirrors indexing the reference engine.io server's clients registry by id.

func (*Server) Sockets

func (s *Server) Sockets() []*ServerSocket

Sockets returns a snapshot of every live session's socket. The slice is a copy taken under the session lock, so it is safe to range over and to call socket methods on while sessions concurrently open and close: a session that ends after the snapshot is taken simply rejects further sends with ErrSocketClosed, and one that opens after it is omitted until the next call. It is the building block for fan-out, since a ServerSocket only ever talks to its own client. This mirrors the reference engine.io server's clients registry.

type ServerCloseHandler

type ServerCloseHandler func(reason string, cause error)

ServerCloseHandler is called once when a session closes. The reason is a short human-readable label (e.g. "transport close", "ping timeout", "forced close"). The cause is the underlying error when one was captured, or nil otherwise; a nil cause does not by itself mean a graceful close (a ping timeout also passes nil), so branch on the reason rather than on whether cause is nil.

type ServerConnectionErrorHandler

type ServerConnectionErrorHandler func(r *http.Request, code ConnectionErrorCode, reason string)

ServerConnectionErrorHandler is called when a handshake is rejected by validation before a session is established. code is the Engine.IO error code (one of the ConnectionError* constants) and reason is the human-readable message (for an allowRequest rejection it is that error's message), so an operator can log or alert on refused connections and branch on why. It is best-effort: a few low-level failures (an unknown-sid WebSocket upgrade, an open-packet build or send failure) tear the connection down without invoking it.

type ServerConnectionHandler

type ServerConnectionHandler func(*ServerSocket)

ServerConnectionHandler is invoked once for each new session, after the open packet has been sent. It is where the application installs the session's message and close handlers and may begin sending. It must not block, since on the websocket transport it runs before the read loop starts.

type ServerMessageHandler

type ServerMessageHandler func(data []byte, isBinary bool)

ServerMessageHandler is invoked for each message a session receives. The isBinary flag reports whether the peer sent the payload as binary, so the application can round-trip binary data without downgrading it to text.

type ServerOption

type ServerOption func(*serverConfig)

ServerOption configures a Server.

func WithAllowRequest

func WithAllowRequest(allowRequest func(r *http.Request) error) ServerOption

WithAllowRequest sets a gate run for every handshake before a session is allocated. Returning a non-nil error rejects the handshake with HTTP 403 and the Engine.IO "Forbidden" error, and the error's message is reported to the connection-error handler. It is the place for authentication, token checks, and rate limiting. Default: unset (every handshake is allowed).

func WithAllowUpgrades

func WithAllowUpgrades(allowUpgrades bool) ServerOption

WithAllowUpgrades determines whether the server advertises and accepts transport upgrades. Default: true.

func WithCORS

func WithCORS(cors CORSOptions) ServerOption

WithCORS configures the cross-origin headers the server sets.

func WithCookie

func WithCookie(cookie CookieOptions) ServerOption

WithCookie enables a session-affinity cookie on the handshake response for sticky sessions behind a cookie-routing load balancer. Empty Name, Path, and SameSite fields default to "io", "/", and Lax. Default: no cookie.

func WithGenerateID

func WithGenerateID(generateID func(r *http.Request) string) ServerOption

WithGenerateID sets the function that returns a new session identifier for a handshake. It receives the handshake request, so the identifier can be derived from request data such as an authenticated user resolved from a header or token. The returned identifier must be unique across live sessions; returning a duplicate of an existing session's identifier is undefined. Default: 18 random bytes, base64url-encoded.

func WithHTTPCompression

func WithHTTPCompression(httpCompression bool) ServerOption

WithHTTPCompression enables or disables gzip compression of long-poll response bodies whose size is at least an internal threshold, when the client advertises gzip in its Accept-Encoding header. Default: true, matching the reference server.

func WithMaxPayload

func WithMaxPayload(maxPayload int) ServerOption

WithMaxPayload sets the maximum accepted POST body size in bytes; a larger body is rejected with HTTP 413. The same limit bounds a single inbound WebTransport frame, which is rejected as a parse error (not an HTTP status) when it exceeds it; a non-positive value still bounds a WebTransport frame at an internal 16 MiB ceiling, so a read can never be left unbounded. Default: 1_000_000.

func WithPingInterval

func WithPingInterval(pingInterval time.Duration) ServerOption

WithPingInterval sets how often the server sends a ping. Default: 25s.

func WithPingTimeout

func WithPingTimeout(pingTimeout time.Duration) ServerOption

WithPingTimeout sets how long the server waits for a pong before closing the session with reason "ping timeout". Default: 20s.

func WithServerTransports

func WithServerTransports(transports ...TransportType) ServerOption

WithServerTransports sets the transports the server accepts. Default: polling and websocket.

func WithUpgradeTimeout

func WithUpgradeTimeout(upgradeTimeout time.Duration) ServerOption

WithUpgradeTimeout bounds how long a transport upgrade probe may take before it is abandoned. Default: 10s.

func WithWebTransportServer

func WithWebTransportServer(wt *webtransport.Server) ServerOption

WithWebTransportServer enables the WebTransport (HTTP/3) transport on the server. The given *webtransport.Server upgrades each Extended CONNECT request into a WebTransport session; the caller runs its HTTP/3 listener (for example wt.ListenAndServeTLS) with this Server mounted on the same handler the TCP listener serves, so polling and websocket continue over TCP while WebTransport runs over UDP. WebTransport must also appear in WithServerTransports for the server to accept it and advertise it as an upgrade target.

It calls webtransport.ConfigureHTTP3Server on wt.H3 so the HTTP/3 server advertises WebTransport in its SETTINGS and installs the request context that Upgrade needs; webtransport-go does not do this automatically, so a caller need not (and a repeat call is harmless).

type ServerSocket

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

ServerSocket is a single connected Engine.IO session, handed to the application through Server.OnConnection.

func (*ServerSocket) Close

func (s *ServerSocket) Close() error

Close gracefully closes the session, delivering a close packet to the client before tearing down.

func (*ServerSocket) ID

func (s *ServerSocket) ID() string

ID returns the session identifier assigned at the handshake. It is stable for the life of the session and safe to read concurrently, since it is set once at construction and never mutated.

func (*ServerSocket) OnClose

func (s *ServerSocket) OnClose(handler ServerCloseHandler)

OnClose registers the handler invoked once when the session closes. It replaces any previously registered handler; passing nil clears it.

func (*ServerSocket) OnMessage

func (s *ServerSocket) OnMessage(handler ServerMessageHandler)

OnMessage registers the handler invoked for each message the session receives. It replaces any previously registered handler; passing nil clears it. It is typically called from the connection handler.

func (*ServerSocket) Send

func (s *ServerSocket) Send(data []byte, isBinary bool) error

Send queues a message for delivery to the client. It is delivered on the next flush: immediately over WebSocket, or on the next poll for long-polling. Send is safe for concurrent use.

type Socket

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

Socket is an Engine.IO v4 client connection to a server. It performs the handshake, runs the server-initiated heartbeat, buffers writes across a transport upgrade, and probes for a better transport in the background. A Socket is configured with SocketOptions, opened with Open, and observed through the OnX handlers; all of its methods are safe for concurrent use.

func NewSocket

func NewSocket(serverURL string, options ...SocketOption) (*Socket, error)

NewSocket creates a Socket for the given server URL, applying the options. It only parses configuration and does not connect; call Open to start the handshake. It returns ErrInvalidURL if serverURL cannot be parsed. The transport registry is snapshotted here, so later changes to Transports do not affect this socket.

Example

ExampleNewSocket builds an Engine.IO client, registers its handlers, and opens it. Sends issued after Open() but before the handshake completes are buffered and flushed once the socket opens; a send before Open() (while the socket is still closed) is dropped. This example sends from OnOpen, after the handshake has completed.

package main

import (
	"context"
	"fmt"
	"net/http"
	"time"

	engineio "github.com/lewisgibson/go-engine.io"
)

func main() {
	ctx := context.Background()

	client, err := engineio.NewSocket("http://localhost:3000/engine.io/",
		engineio.WithClient(&http.Client{Timeout: 30 * time.Second}),
		engineio.WithUpgrade(true),
		engineio.WithTransports(
			engineio.TransportTypePolling,
			engineio.TransportTypeWebSocket,
		),
	)
	if err != nil {
		panic(err)
	}

	client.OnOpen(func() {
		if err := client.Send(ctx, []engineio.Packet{
			{Type: engineio.PacketMessage, Data: []byte("Hello")},
		}); err != nil {
			fmt.Printf("send error: %v\n", err)
		}
	})
	client.OnMessage(func(data []byte, isBinary bool) {
		fmt.Printf("message (binary=%t): %s\n", isBinary, string(data))
	})
	client.OnPacket(func(packet engineio.Packet) {
		fmt.Printf("packet: %s\n", packet)
	})
	client.OnUpgrade(func(transportType engineio.TransportType) {
		fmt.Printf("upgraded to: %s\n", transportType)
	})
	client.OnError(func(err error) {
		fmt.Printf("error: %v\n", err)
	})
	client.OnClose(func(reason string, cause error) {
		fmt.Printf("close: %s (%v)\n", reason, cause)
	})

	// Open and close are omitted here so the example does not connect.
	_ = client
}

func (*Socket) Close

func (s *Socket) Close(ctx context.Context)

Close closes the socket, asking the active transport to send a close packet and tear down. It is a no-op unless the socket is open or opening, so a duplicate Close is harmless. The close handler fires once the teardown completes.

Anything still in the write buffer is flushed before the transport is torn down, so a message sent immediately before Close is delivered rather than dropped. A flush is only attempted while the socket is open and not mid-upgrade (the buffer is flushed over the new transport when an upgrade completes).

func (*Socket) OnClose

func (s *Socket) OnClose(handler SocketCloseHandler)

OnClose registers the handler invoked when the socket closes. It replaces any previously registered handler; passing nil clears it.

func (*Socket) OnError

func (s *Socket) OnError(handler SocketErrorHandler)

OnError registers the handler invoked when the socket encounters an error. It replaces any previously registered handler; passing nil clears it. An error is not always fatal, so the handler should not assume the socket has closed.

func (*Socket) OnMessage

func (s *Socket) OnMessage(handler SocketMessageHandler)

OnMessage registers the handler invoked for each message packet the socket receives. It replaces any previously registered handler; passing nil clears it. This is the handler most applications use.

func (*Socket) OnOpen

func (s *Socket) OnOpen(handler SocketOpenHandler)

OnOpen registers the handler invoked when the socket opens. It replaces any previously registered handler; passing nil clears it. It is safe to call concurrently, though handlers are normally set before Open.

func (*Socket) OnPacket

func (s *Socket) OnPacket(handler SocketPacketHandler)

OnPacket registers the handler invoked for every packet the socket receives, including protocol packets. It replaces any previously registered handler; passing nil clears it. Use OnMessage for application data only.

func (*Socket) OnUpgrade

func (s *Socket) OnUpgrade(handler SocketUpgradeHandler)

OnUpgrade registers the handler invoked when the socket upgrades to a new transport. It replaces any previously registered handler; passing nil clears it.

func (*Socket) OnUpgradeError

func (s *Socket) OnUpgradeError(handler SocketUpgradeErrorHandler)

OnUpgradeError registers the handler invoked when an upgrade probe fails. It replaces any previously registered handler; passing nil clears it.

func (*Socket) Open

func (s *Socket) Open(ctx context.Context)

Open connects the socket: it creates the first transport, opens it, and drives the handshake. The given context bounds the connection's lifetime; cancelling it tears down the active transport and any in-flight upgrade probe. Open is a no-op unless the socket is closed, so it is safe to call once and to reopen after a close. Failure to create a transport is reported through the error handler rather than returned.

func (*Socket) Send

func (s *Socket) Send(ctx context.Context, packets []Packet) error

Send appends packets to the write buffer and flushes it. Packets sent while the socket is still opening are buffered and flushed once it opens; while a transport upgrade is in progress the flush is deferred so they are sent over the new transport once the switch completes. This mirrors the reference client and keeps writes from being lost or reordered. Sends on a closing or closed socket are dropped, matching the reference's fire-and-forget contract.

type SocketCloseHandler

type SocketCloseHandler func(reason string, cause error)

SocketCloseHandler is invoked once when the socket closes. The reason is a short human-readable description and the cause is the underlying error that triggered the close, or nil when no error was involved -- both a graceful close and a ping timeout pass a nil cause, so branch on the reason, not on whether cause is nil. It runs on a transport goroutine.

type SocketErrorHandler

type SocketErrorHandler func(error)

SocketErrorHandler is invoked when the socket encounters an error, such as a transport failure or a malformed packet. An error does not always close the socket: a failed upgrade probe is non-fatal and leaves the current transport running. A probe failure is delivered to the upgrade-error handler when one is set (see OnUpgradeError) and only falls back to this handler otherwise. It runs on a transport goroutine and must not block.

type SocketMessageHandler

type SocketMessageHandler func(data []byte, isBinary bool)

SocketMessageHandler is called when the socket receives a message packet. The isBinary flag reports whether the peer sent the payload as a binary frame, so the application can round-trip binary data without downgrading it to text.

type SocketOpenHandler

type SocketOpenHandler func()

SocketOpenHandler is invoked once when the socket opens, after the handshake completes. It is the point at which the application can safely start sending. It runs on a transport goroutine and must not block.

type SocketOption

type SocketOption func(*socketConfig)

SocketOption configures a Socket.

func WithClient

func WithClient(client TransportClient) SocketOption

WithClient sets the HTTP client used by the socket's transports. Default: a new http.Client.

func WithHeader

func WithHeader(header http.Header) SocketOption

WithHeader sets the headers sent by the socket's transports. Default: an empty http.Header.

func WithRememberUpgrade

func WithRememberUpgrade(rememberUpgrade bool) SocketOption

WithRememberUpgrade determines whether the socket reuses a previous successful upgrade on the next connection. Default: false.

func WithTransports

func WithTransports(transports ...TransportType) SocketOption

WithTransports sets the transports the socket tries, in order. Default: polling then websocket.

func WithTryAllTransports

func WithTryAllTransports(tryAllTransports bool) SocketOption

WithTryAllTransports determines whether the socket tries every transport in the list before giving up. Default: false.

func WithUpgrade

func WithUpgrade(upgrade bool) SocketOption

WithUpgrade determines whether the socket tries to upgrade from long-polling to a better transport. Default: true.

func WithWebTransportDialer

func WithWebTransportDialer(dialer *webtransport.Dialer) SocketOption

WithWebTransportDialer enables the WebTransport (HTTP/3) transport on the client, dialing sessions with the given *webtransport.Dialer (which carries the TLS and QUIC configuration). WebTransport must also appear in WithTransports for the socket to try it, either as the initial transport or as an upgrade target.

type SocketPacketHandler

type SocketPacketHandler func(Packet)

SocketPacketHandler is invoked for every packet the socket receives, including protocol packets such as ping and open, in arrival order. Use OnMessage to receive only application data; this handler is for callers that need to observe the raw protocol. It runs on a transport goroutine and must not block.

type SocketState

type SocketState string

SocketState is the lifecycle state of a client Socket. The Socket guards it under its lock and uses it to make Open, Close, Send, and the upgrade flow idempotent and to decide whether buffered writes may flush.

const (
	// SocketStateOpen indicates the handshake has completed and the socket can send
	// and receive packets.
	SocketStateOpen SocketState = "open"
	// SocketStateOpening indicates the socket is connecting and awaiting the
	// server's open packet. Writes made now are buffered until it opens.
	SocketStateOpening SocketState = "opening"
	// SocketStateClosed indicates the socket is fully torn down. It is the initial
	// state and the state a Socket can be reopened from.
	SocketStateClosed SocketState = "closed"
	// SocketStateClosing indicates a close is in progress; the socket no longer
	// accepts writes and is delivering its final packets before closing.
	SocketStateClosing SocketState = "closing"
)

type SocketUpgradeErrorHandler

type SocketUpgradeErrorHandler func(error)

SocketUpgradeErrorHandler is invoked when an upgrade probe fails. A failed probe is non-fatal: the socket keeps running on its current transport, so this is distinct from the error handler (used for fatal transport errors) and lets an application detect, e.g. a proxy that blocks WebSocket. When no upgrade-error handler is set, a probe failure is reported to the error handler instead.

type SocketUpgradeHandler

type SocketUpgradeHandler func(transportType TransportType)

SocketUpgradeHandler is invoked when the socket finishes upgrading to a new transport, with the type it switched to. It fires after the switch is committed and before buffered writes are flushed over the new transport, so the application can observe the better transport taking over.

type Transport

type Transport interface {
	// Type reports which transport kind this is, so the Socket can decide whether
	// an offered upgrade is worth probing and record a successful WebSocket upgrade.
	Type() TransportType
	// State reports the transport's current lifecycle state. It is a snapshot taken
	// under the transport's lock; the state may change immediately after it returns.
	State() TransportState

	// SetURL replaces the URL the transport requests. The Socket calls it after the
	// handshake to attach the negotiated session id to subsequent requests. It is
	// safe to call concurrently with the transport's own requests.
	SetURL(url *url.URL)

	// Open starts the transport: a polling transport issues its first long-poll, and
	// a WebSocket or WebTransport transport dials and begins reading. It transitions
	// the transport from closed to open and is a no-op if the transport is not
	// closed, so a duplicate Open cannot start a second connection. OnOpen fires once
	// the transport is ready.
	Open(ctx context.Context)
	// Close shuts the transport down, sending a best-effort close packet to the peer
	// and tearing down the underlying connection. It is idempotent and fires OnClose
	// at most once; a close already triggered by the peer is not duplicated.
	Close(ctx context.Context)
	// Pause stops a transport from sending or receiving and waits for any in-flight
	// work to drain. The Socket calls it on the old transport during an upgrade so a
	// packet that transport is mid-delivery is delivered before traffic moves to the
	// new transport, preserving ordering. For a transport with nothing to drain
	// (WebSocket or WebTransport) it is a no-op.
	Pause(ctx context.Context)

	// Send writes packets to the peer. A polling transport sends them as one POST; a
	// WebSocket transport writes one frame per packet; a WebTransport transport writes
	// length-framed packets on its stream. Packets sent while the transport is not
	// open are dropped (returning nil) rather than erroring, so the Socket's buffering
	// decides what is retained. It returns an error only when a write actually fails.
	Send(ctx context.Context, packets []Packet) error

	// OnOpen registers the handler invoked when the transport opens. It replaces any
	// previously registered handler; passing nil clears it. The Socket clears the
	// handlers before closing a transport so the teardown cannot re-enter the Socket.
	OnOpen(handler TransportOpenHandler)
	// OnClose registers the handler invoked when the transport closes. It replaces
	// any previously registered handler; passing nil clears it.
	OnClose(handler TransportCloseHandler)
	// OnPacket registers the handler invoked for each packet the transport receives.
	// It replaces any previously registered handler; passing nil clears it.
	OnPacket(handler TransportPacketHandler)
	// OnError registers the handler invoked when the transport encounters an error.
	// It replaces any previously registered handler; passing nil clears it.
	OnError(handler TransportErrorHandler)
}

Transport is the client side of a single Engine.IO transport (long-polling, WebSocket, or WebTransport). The Socket drives a transport through its lifecycle and reacts to the transport's events through the OnX handlers; a transport never interprets packets itself. All methods are safe for concurrent use, and the OnX handlers fire on the transport's own goroutines.

func NewPollingTransport

func NewPollingTransport(url *url.URL, client TransportClient, header http.Header) (Transport, error)

NewPollingTransport creates a new PollingTransport. A nil client defaults to http.DefaultClient and a nil header to an empty header.

func NewWebSocketTransport

func NewWebSocketTransport(url *url.URL, client TransportClient, header http.Header) (Transport, error)

NewWebSocketTransport creates a new WebSocket transport. A nil client defaults to http.DefaultClient and a nil header to an empty header.

func NewWebTransportTransport

func NewWebTransportTransport(target *url.URL, dialer *webtransport.Dialer, header http.Header) (Transport, error)

NewWebTransportTransport creates a WebTransport transport that dials sessions with dialer (which carries the TLS and QUIC configuration). A nil header defaults to an empty header; a nil dialer or URL is an error. It is exported for parity with NewPollingTransport and NewWebSocketTransport, but is not in the Transports registry because it needs a dialer the registry's constructor signature does not carry; WithWebTransportDialer wires it per socket.

type TransportClient

type TransportClient interface {
	Do(req *http.Request) (*http.Response, error)
}

TransportClient is the minimal HTTP surface a transport needs: a way to execute a request. *http.Client satisfies it, and it is accepted (rather than the concrete type) so callers can inject a custom client or a mock in tests.

type TransportCloseHandler

type TransportCloseHandler func(context.Context)

TransportCloseHandler is invoked once when a transport closes, whether the close was requested locally or observed from the peer. It runs on a transport goroutine and must not block. Transports guarantee it fires at most once.

type TransportConstructor

type TransportConstructor func(url *url.URL, client TransportClient, header http.Header) (Transport, error)

TransportConstructor builds a transport for a target URL using the given client and headers. The Transports registry maps each TransportType to its constructor, and the Socket calls the matching one when opening or upgrading.

type TransportErrorHandler

type TransportErrorHandler func(context.Context, error)

TransportErrorHandler is invoked when a transport encounters an error, such as a failed request or a read failure. It runs on a transport goroutine and must not block. An error is typically followed by the transport closing.

type TransportOpenHandler

type TransportOpenHandler func(context.Context)

TransportOpenHandler is invoked once when a transport finishes opening, before any packet is delivered. It runs on a transport goroutine, so it must not block; the Socket uses it to drive the upgrade handshake and flush buffered writes.

type TransportPacketHandler

type TransportPacketHandler func(context.Context, Packet)

TransportPacketHandler is invoked for each packet a transport receives, in the order the packets arrive. It runs on the transport's read or poll goroutine, so a slow handler stalls further reads on that transport; it must not block indefinitely.

type TransportRoundTripper

type TransportRoundTripper struct {
	// Client is the underlying client requests are forwarded to. RoundTrip returns
	// ErrTransportRoundTripperClientRequired if it is nil.
	Client TransportClient
}

TransportRoundTripper adapts a TransportClient into an http.RoundTripper. The WebSocket transport needs an *http.Client to dial, so a caller-supplied TransportClient is wrapped here rather than mutating the shared http.DefaultClient.

func (*TransportRoundTripper) RoundTrip

func (t *TransportRoundTripper) RoundTrip(req *http.Request) (*http.Response, error)

RoundTrip forwards the request to the wrapped TransportClient, satisfying http.RoundTripper. It returns ErrTransportRoundTripperClientRequired when no client was set.

type TransportState

type TransportState string

TransportState is the lifecycle state of a transport. Transports advance through these states under their own lock and use them to make Open, Close, and Pause idempotent and to decide whether a poll or write may proceed.

const (
	// TransportStateOpening represents a transport that is opening.
	TransportStateOpening TransportState = "opening"
	// TransportStateOpen represents an open transport.
	TransportStateOpen TransportState = "open"
	// TransportStateClosing represents a transport that is closing.
	TransportStateClosing TransportState = "closing"
	// TransportStateClosed represents a transport that is closed.
	TransportStateClosed TransportState = "closed"
	// TransportStatePausing represents a transport that is pausing.
	TransportStatePausing TransportState = "pausing"
	// TransportStatePaused represents a transport that is paused.
	TransportStatePaused TransportState = "paused"
)

func (TransportState) String

func (s TransportState) String() string

String returns the state name for logging and test output.

type TransportType

type TransportType string

TransportType names a transport kind. It doubles as the wire value of the "transport" query parameter and the entries of an open packet's upgrade list, so its string form is the protocol name rather than a Go identifier.

const (
	// TransportTypePolling represents a polling transport.
	TransportTypePolling TransportType = "polling"
	// TransportTypeWebSocket represents a WebSocket transport.
	TransportTypeWebSocket TransportType = "websocket"
	// TransportTypeWebTransport represents a WebTransport (HTTP/3) transport.
	TransportTypeWebTransport TransportType = "webtransport"
)

func (TransportType) String

func (t TransportType) String() string

String returns the transport name as it appears on the wire ("polling", "websocket", or "webtransport").

type WebSocketTransport

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

WebSocketTransport is a transport that uses the WebSocket protocol.

func (*WebSocketTransport) Close

func (t *WebSocketTransport) Close(ctx context.Context)

Close sends a best-effort close packet and tears down the connection. The close packet is only written while the transport is open; a Close during the opening window tears the connection down without sending one.

func (*WebSocketTransport) OnClose

func (t *WebSocketTransport) OnClose(handler TransportCloseHandler)

OnClose sets the handler for when the transport closes.

func (*WebSocketTransport) OnError

func (t *WebSocketTransport) OnError(handler TransportErrorHandler)

OnError sets the handler for when the transport encounters an error.

func (*WebSocketTransport) OnOpen

func (t *WebSocketTransport) OnOpen(handler TransportOpenHandler)

OnOpen sets the handler for when the transport opens.

func (*WebSocketTransport) OnPacket

func (t *WebSocketTransport) OnPacket(handler TransportPacketHandler)

OnPacket sets the handler for when the transport receives packets.

func (*WebSocketTransport) Open

func (t *WebSocketTransport) Open(ctx context.Context)

Open dials the WebSocket connection and starts reading frames.

func (*WebSocketTransport) Pause

func (t *WebSocketTransport) Pause(_ context.Context)

Pause is a no-op for the WebSocket transport, which has no polling to drain.

func (*WebSocketTransport) Send

func (t *WebSocketTransport) Send(ctx context.Context, packets []Packet) error

Send writes each packet as its own frame: binary messages as raw binary frames, everything else as text frames.

func (*WebSocketTransport) SetURL

func (t *WebSocketTransport) SetURL(url *url.URL)

SetURL sets the URL for the transport.

func (*WebSocketTransport) State

State returns the state of the transport.

func (*WebSocketTransport) Type

Type returns the type of the transport.

type WebTransportTransport

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

WebTransportTransport is a transport that carries Engine.IO packets over a single bidirectional stream of an HTTP/3 WebTransport session.

func (*WebTransportTransport) Close

func (t *WebTransportTransport) Close(ctx context.Context)

Close sends a best-effort close packet and tears the session down. The close packet is only written while the transport is open.

func (*WebTransportTransport) OnClose

func (t *WebTransportTransport) OnClose(handler TransportCloseHandler)

OnClose sets the handler for when the transport closes.

func (*WebTransportTransport) OnError

func (t *WebTransportTransport) OnError(handler TransportErrorHandler)

OnError sets the handler for when the transport encounters an error.

func (*WebTransportTransport) OnOpen

func (t *WebTransportTransport) OnOpen(handler TransportOpenHandler)

OnOpen sets the handler for when the transport opens.

func (*WebTransportTransport) OnPacket

func (t *WebTransportTransport) OnPacket(handler TransportPacketHandler)

OnPacket sets the handler for when the transport receives packets.

func (*WebTransportTransport) Open

func (t *WebTransportTransport) Open(ctx context.Context)

Open dials the WebTransport session, opens the bidirectional stream, writes the opening packet that identifies the session to the server, and starts reading.

func (*WebTransportTransport) Pause

Pause is a no-op for the WebTransport transport, which has no polling to drain.

func (*WebTransportTransport) Send

func (t *WebTransportTransport) Send(ctx context.Context, packets []Packet) error

Send frames each packet and writes them to the stream. Packets sent while the transport is not open are dropped, leaving retention to the socket's buffer.

func (*WebTransportTransport) SetURL

func (t *WebTransportTransport) SetURL(url *url.URL)

SetURL sets the URL for the transport.

func (*WebTransportTransport) State

State returns the state of the transport.

func (*WebTransportTransport) Type

Type returns the type of the transport.

Directories

Path Synopsis
Package internal holds implementation utilities that are not part of the public go-engine.io API.
Package internal holds implementation utilities that are not part of the public go-engine.io API.
test
interop
Package interop holds build-tagged interoperability tests that drive this Go library against the canonical JavaScript Engine.IO server and client.
Package interop holds build-tagged interoperability tests that drive this Go library against the canonical JavaScript Engine.IO server and client.

Jump to

Keyboard shortcuts

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