mqtt

package module
v1.2.0 Latest Latest
Warning

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

Go to latest
Published: Jul 7, 2026 License: MIT Imports: 19 Imported by: 0

README

go-mqtt

ci Go Reference

Minimal pure-Go MQTT client: a dual-version wire codec (MQTT 5.0 by default, MQTT 3.1.1 supported), a TCP/TLS adapter with QoS 0/1/2, session resumption and flow control, and a reconnecting lifecycle wrapper. No third-party dependencies — standard library only, in the module and in its tests.

This module is the shared transport layer factored out of the go-*2mqtt bridges (go-mtec2mqtt, go-zendure2mqtt, go-homeconnect2mqtt) and openccu-loom, where it previously lived duplicated as internal/mqtt.

Upgrading from v0.x? See MIGRATION.md — the API, defaults and a few behaviors changed in v1.0.

Features

Feature Support
MQTT 5.0 Default protocol (ProtocolV50)
MQTT 3.1.1 Supported via ProtocolVersion: ProtocolV311
QoS 0, 1 and 2, both directions, full PUBREC/PUBREL/PUBCOMP handshake
Session resumption CleanStart=false + Session Expiry; unacked QoS>0 state and inbound QoS 2 dedup state replay in order on a resumed session
Flow control Receive Maximum (v5) / configurable MaxInflight (v3.1.1) enforced as a send-quota semaphore; broker Maximum QoS / Retain Available honored locally
Inbound topic aliases v5 topic alias table resolved per connection (outbound aliasing is out of scope)
Last Will and Testament v3.1.1 topic/payload/QoS/retain; v5 adds will properties (delay interval, message expiry, content type, correlation data, user properties)
TLS NewClientTLSConfig helper — always sets ServerName, never defaults to InsecureSkipVerify
Reconnect lifecycle Lifecycle: exponential backoff + jitter, event-driven via ConnectionLost() (immediate reconnect + backoff reset on a detected drop, not just idle polling)
Dependencies Zero third-party — standard library only, including tests

Packages

  • protocol — CONNECT/CONNACK, PUBLISH + PUBACK/PUBREC/PUBREL/PUBCOMP, SUBSCRIBE/SUBACK, UNSUBSCRIBE/UNSUBACK, PINGREQ/PINGRESP, DISCONNECT/AUTH encoding and decoding for both MQTT 3.1.1 and MQTT 5.0, plus the v5 property model, reason codes and topic matching/validation. See protocol/doc.go for the exact feature/omission list.
  • mqtt (module root) — TCPClient, a Connector/Client implementation that dials a tcp:// or tls:// broker, drives the QoS 1/2 state machine and session replay, resolves inbound topic aliases, and watches PINGRESP to detect half-open sockets; and Lifecycle, a reconnect loop around any Connector.

License

MIT. The client was originally written for openccu-loom; file headers keep that provenance.

Usage

MQTT 5.0 is the default protocol — leave ProtocolVersion unset:

client := mqtt.NewTCPClient(mqtt.TCPConfig{
	BrokerURL:  "tcp://broker.example.com:1883",
	ClientID:   "my-bridge",
	CleanStart: true,
	KeepAlive:  30 * time.Second,
	Will: &mqtt.Will{
		Topic:   "bridge/status",
		Payload: []byte("offline"),
		QoS:     mqtt.QoS1,
		Retain:  true,
	},
})

lc := mqtt.NewLifecycle(mqtt.DefaultLifecycle(), client)
lc.OnConnect(func(ctx context.Context) {
	_, _ = client.Subscribe(ctx, "cmd/#", mqtt.QoS1, func(msg *mqtt.Message) {
		// A handler with side effects should skip retained replays
		// (msg.Retain == true) the broker re-delivers on every (re)connect.
		if msg.Retain {
			return
		}
		// handle msg.Topic / msg.Payload — return quickly, see the
		// MessageHandler contract on the type for why.
	})
})

// Start blocks until the first connect succeeds (or ctx is done); ctx
// then governs the WHOLE reconnect loop, not just this call.
if err := lc.Start(context.Background()); err != nil {
	log.Fatal(err)
}
defer lc.Stop(context.Background())

_ = client.Publish(context.Background(), "bridge/status", []byte("online"), mqtt.QoS1, true)

For a tls:// broker, set TCPConfig.TLSConfigNewClientTLSConfig builds a *tls.Config with the mandatory ServerName set correctly.

MQTT 3.1.1 and v5 properties

Pin ProtocolV311 for a broker that doesn't speak MQTT 5.0 yet. On a v5 link, PublishOption/SubscribeOption attach protocol properties (they are silently no-ops on a v3.1.1 link):

client311 := mqtt.NewTCPClient(mqtt.TCPConfig{
	BrokerURL:       "tcp://legacy-broker.example.com:1883",
	ClientID:        "my-bridge",
	ProtocolVersion: mqtt.ProtocolV311, // downgrade from the v5 default
	MaxInflight:     20,                // v3.1.1's Receive-Maximum equivalent
})

_ = client311.Publish(context.Background(), "bridge/status", []byte("online"),
	mqtt.QoS1, true,
	mqtt.WithUserProperties(mqtt.UserProperty{Key: "source", Value: "my-bridge"}),
)

Circuit breaker

Breaker wraps any Publisher and fails fast when the broker is up but unhealthy — the case the reconnect loop cannot detect. Without it, a broker that stops acknowledging turns every QoS >= 1 publish into an AckTimeout-long stall.

breaker := mqtt.NewBreaker(client, mqtt.BreakerConfig{
	FailureThreshold: 5,                // consecutive failures to open
	RecoveryTimeout:  30 * time.Second, // fail-fast window before probing
	HalfOpenMax:      1,                // concurrent recovery probes
	OnStateChange: func(from, to mqtt.BreakerState) {
		slog.Warn("mqtt breaker", "from", from.String(), "to", to.String())
	},
})
// Publish through the breaker instead of the raw client:
err := breaker.Publish(ctx, "state/topic", payload, mqtt.QoS1, false)
if errors.Is(err, mqtt.ErrCircuitOpen) {
	// broker unhealthy — dropped fast, no AckTimeout stall
}

Ack timeouts, ErrConnectionLost, ErrNotConnected and broker rejects (*ReasonError with an error reason code) count as failures; caller-side context cancellation and local limit violations (ErrPacketTooLarge, ErrPacketIDExhausted) are neutral.

Testing

make test          # race-detector unit + integration tests (mock broker), no network
make test-cover     # tests + coverage report
make cover-check    # per-package coverage gate (COVER_MIN=90 by default)
make fuzz-smoke     # 10s per protocol Fuzz target — cheap CI regression net
make fuzz           # FUZZTIME per target (default 5m) — longer local/CI fuzz run
make check          # vet + fmt-check + lint + test — the pre-commit/pre-push gate

The e2e/ package exercises real brokers over Docker (mosquitto and EMQX, both protocol versions, TLS, auth, QoS 0/1/2, retained replay, LWT, session resumption, reconnection). It is gated by environment variables and skips itself when no broker is reachable:

make e2e-up      # start mosquitto (plain/TLS/password listeners) + EMQX via docker
make test-e2e    # run ./e2e against the containers started above
make e2e-down    # stop and remove them

See the Development Commands section of CLAUDE.md for the full target list.

Migrating from v0.x

v1.0 defaults to MQTT 5.0, makes Subscribe block until SUBACK, and changes several config field names and error-handling behaviors. See MIGRATION.md for the full old→new signature table, before/after diffs and behavior changes.

Documentation

Overview

Package mqtt is a minimal, dependency-free MQTT 3.1.1 and 5.0 client library: a reconnecting TCP/TLS adapter over the wire codec in github.com/SukramJ/go-mqtt/protocol. The root package defines the version-agnostic client contracts (Publisher, Subscriber, Client), the inbound Message shape and MessageHandler dispatch rules, the publish/subscribe options, and the sentinel errors consumers match on.

Index

Constants

View Source
const SessionExpiryNever uint32 = 0xFFFFFFFF

SessionExpiryNever is the MQTT 5.0 Session Expiry Interval value that requests a session which never expires (0xFFFFFFFF, §3.1.2.11.2).

Variables

View Source
var (
	// ErrNotConnected is returned by Publish/Subscribe/Unsubscribe when no
	// session is currently established.
	ErrNotConnected = errors.New("mqtt: not connected")

	// ErrAlreadyConnected is returned (wrapped) by Connect when a session
	// is already established; [Lifecycle] treats it as an idempotent
	// no-op rather than a reconnect trigger.
	ErrAlreadyConnected = errors.New("mqtt: already connected")

	// ErrConnectionLost is returned to every in-flight Publish/Subscribe
	// waiter when the underlying connection drops before its
	// acknowledgement arrives.
	ErrConnectionLost = errors.New("mqtt: connection lost")

	// ErrPacketTooLarge is returned when an outbound packet would exceed
	// the broker-advertised Maximum Packet Size.
	ErrPacketTooLarge = errors.New("mqtt: packet exceeds maximum packet size")

	// ErrPacketIDExhausted is returned when no MQTT packet identifier is
	// free, i.e. all 65535 non-zero identifiers are in use by in-flight
	// QoS 1/2 publishes and pending SUBSCRIBE/UNSUBSCRIBE requests.
	ErrPacketIDExhausted = errors.New("mqtt: packet identifiers exhausted")
)

Sentinel errors returned by the client. Callers match them with errors.Is; a broker-supplied failure reason surfaces as a *ReasonError matched with errors.As.

View Source
var ErrCircuitOpen = errors.New("mqtt: circuit open")

ErrCircuitOpen is returned by Breaker.Publish while the circuit is open: the broker link has produced too many consecutive failures and callers fail fast instead of each blocking on the AckTimeout.

Functions

func NewClientTLSConfig

func NewClientTLSConfig(serverName string, insecureSkipVerify bool) *tls.Config

NewClientTLSConfig builds the *tls.Config to hand to TCPConfig.TLSConfig for a tls:// broker connection.

serverName is mandatory: tls.Client(conn, cfg) does NOT infer ServerName from the dialed address, so a config built without it fails certificate-hostname verification for every broker — a gap that is easy to "fix" by reaching for InsecureSkipVerify instead of setting ServerName. NewClientTLSConfig always derives ServerName from the configured broker host so that trap does not exist.

insecureSkipVerify must only be true when the operator has explicitly opted in (config key MQTT_SSL_INSECURE) for a broker with a self-signed certificate the operator controls. It must never default to true — doing so would silently accept any certificate, including one presented by a man-in-the-middle.

Types

type Breaker added in v1.1.0

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

Breaker is a circuit-breaking Publisher decorator.

It addresses the degraded-broker case the reconnect loop cannot see: the TCP link is up, but the broker stops acknowledging, so every QoS >= 1 publish blocks for the full AckTimeout. After FailureThreshold consecutive countable failures the circuit opens and publishes return ErrCircuitOpen immediately; after RecoveryTimeout a bounded number of probes is let through, and one success closes the circuit again.

Countable failures are broker-side symptoms: acknowledgement timeouts, ErrConnectionLost, ErrNotConnected and broker rejects (*ReasonError). Local conditions — caller context cancellation, ErrPacketTooLarge, ErrPacketIDExhausted, client-side limit violations — never trip the circuit.

A Breaker is safe for concurrent use and adds no overhead beyond one mutex acquisition per publish.

func NewBreaker added in v1.1.0

func NewBreaker(pub Publisher, cfg BreakerConfig) *Breaker

NewBreaker wraps pub in a circuit breaker. pub must not be nil.

func (*Breaker) Publish added in v1.1.0

func (b *Breaker) Publish(ctx context.Context, topic string, payload []byte, qos QoS, retain bool, opts ...PublishOption) error

Publish implements Publisher with circuit gating.

func (*Breaker) State added in v1.1.0

func (b *Breaker) State() BreakerState

State returns the current circuit state.

type BreakerConfig added in v1.1.0

type BreakerConfig struct {
	// FailureThreshold is the number of consecutive countable publish
	// failures that opens the circuit. Zero or negative uses 5.
	FailureThreshold int
	// RecoveryTimeout is how long an open circuit rejects publishes
	// before the next attempt is admitted as a half-open probe. Zero
	// or negative uses 30 seconds.
	RecoveryTimeout time.Duration
	// HalfOpenMax bounds the number of concurrent probe publishes in
	// the half-open state; excess publishes keep failing fast. Zero or
	// negative uses 1.
	HalfOpenMax int
	// OnStateChange, when non-nil, is called synchronously after every
	// state transition, outside the breaker's lock. Wire metrics or
	// logging here.
	OnStateChange func(from, to BreakerState)
	// contains filtered or unexported fields
}

BreakerConfig tunes a Breaker. The zero value is usable: 5 consecutive failures open the circuit, recovery is probed after 30 seconds with a single in-flight probe.

type BreakerState added in v1.1.0

type BreakerState int

BreakerState is the circuit state of a Breaker.

const (
	// BreakerClosed passes every publish through (healthy).
	BreakerClosed BreakerState = iota
	// BreakerOpen fails every publish fast with [ErrCircuitOpen].
	BreakerOpen
	// BreakerHalfOpen lets a bounded number of probe publishes through
	// to test whether the broker recovered.
	BreakerHalfOpen
)

Breaker states.

func (BreakerState) String added in v1.1.0

func (s BreakerState) String() string

String returns the lowercase state name.

type Client

type Client interface {
	Publisher
	Subscriber
}

Client is the combined role a bridge uses. Most real adapters satisfy both; the split exists to make testing narrow facades easier.

type ConnectResult added in v1.0.0

type ConnectResult struct {
	// SessionPresent reports whether the broker resumed an existing
	// session (§3.2.2.1.1).
	SessionPresent bool
	// ReasonCode is the CONNACK reason code (Success on a normal accept).
	ReasonCode ReasonCode
	// AssignedClientID is the client identifier the broker assigned when
	// the client connected with an empty one (MQTT 5.0 property 0x12);
	// empty otherwise.
	AssignedClientID string
	// ServerKeepAlive is the keep-alive interval the broker imposed
	// (MQTT 5.0 property 0x13); zero when the broker did not override the
	// requested value.
	ServerKeepAlive time.Duration
	// ReceiveMaximum is the number of unacknowledged QoS 1/2 publishes the
	// broker will accept concurrently (MQTT 5.0 property 0x21); defaults
	// to 65535.
	ReceiveMaximum uint16
	// MaximumQoS is the highest QoS the broker supports (MQTT 5.0 property
	// 0x24); defaults to QoS 2.
	MaximumQoS QoS
	// RetainAvailable reports whether the broker supports retained
	// messages (MQTT 5.0 property 0x25); defaults to true.
	RetainAvailable bool
	// MaximumPacketSize is the largest packet the broker will accept
	// (MQTT 5.0 property 0x27); zero when the broker set no limit.
	MaximumPacketSize uint32
	// TopicAliasMaximum is the highest topic alias the broker accepts on
	// outbound publishes (MQTT 5.0 property 0x22); zero disables aliases.
	TopicAliasMaximum uint16
	// UserProperties holds any MQTT 5.0 User Property pairs (0x26) the
	// broker returned in the CONNACK.
	UserProperties []UserProperty
}

ConnectResult is the negotiated session state decoded from the CONNACK. The MQTT 5.0 fields reflect the broker's advertised limits; on an MQTT 3.1.1 link they carry their protocol defaults.

type ConnectionNotifier added in v1.0.0

type ConnectionNotifier interface {
	ConnectionLost() <-chan struct{}
}

ConnectionNotifier is an optional capability a Connector may also satisfy to drive the reconnect loop event-driven instead of purely timer-polled. [ConnectionLost] returns a channel that receives a value whenever the underlying session drops. [Lifecycle.loop] selects on it so a detected drop triggers an immediate reconnect attempt rather than waiting out the (possibly LifecycleConfig.MaxBackoff) idle-probe timer. TCPClient implements this via its buffered, non-blocking ConnectionLost channel.

type Connector

type Connector interface {
	Connect(ctx context.Context) error
	Disconnect(ctx context.Context) error
}

Connector is the narrow lifecycle contract a broker adapter must satisfy. `Connect` establishes the session (including the LWT handshake); `Disconnect` unregisters the session gracefully.

type Lifecycle

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

Lifecycle drives a Connector with automatic reconnect. It is intentionally transport-agnostic: paho, nhooyr, or any future adapter just implements Connector.

func NewLifecycle

func NewLifecycle(cfg LifecycleConfig, connector Connector) *Lifecycle

NewLifecycle constructs a lifecycle around connector.

func (*Lifecycle) OnConnect

func (l *Lifecycle) OnConnect(fn func(context.Context))

OnConnect registers a callback fired on every successful (re)connect. Typical use: `bridge.AnnounceOnline` + resubscribe.

func (*Lifecycle) Start

func (l *Lifecycle) Start(ctx context.Context) error

Start boots the reconnect loop and returns once the first connect has succeeded (or ctx was cancelled). Subsequent drops reconnect in the background.

ctx governs the WHOLE reconnect loop, not just the initial connect: cancelling it permanently stops reconnection (without disconnecting an established session — use Lifecycle.Stop for an orderly shutdown). Pass a context that lives as long as reconnection should, typically the application's run context — never a short-lived timeout context, or the loop silently dies with it.

func (*Lifecycle) Stop

func (l *Lifecycle) Stop(ctx context.Context) error

Stop cancels the loop and disconnects the session.

type LifecycleConfig

type LifecycleConfig struct {
	InitialBackoff time.Duration
	MaxBackoff     time.Duration
	Jitter         time.Duration
	Logger         *slog.Logger
}

LifecycleConfig governs the reconnect loop.

func DefaultLifecycle

func DefaultLifecycle() LifecycleConfig

DefaultLifecycle returns the MVP default timings: 1s → 30s exponential backoff with ±500ms jitter.

type Message added in v1.0.0

type Message struct {
	// Topic is the topic name the message was published to. For an
	// inbound PUBLISH that used a topic alias, the alias has already been
	// resolved to the original topic name.
	Topic string
	// Payload is the application message bytes.
	Payload []byte
	// QoS is the delivery quality of service of this message.
	QoS QoS
	// Retain is the PUBLISH retain bit (§3.3.1.3): true when the broker
	// re-delivered this as the retained message for the filter on
	// (re)subscribe rather than as a live publish. See [MessageHandler].
	Retain bool
	// Dup is the PUBLISH duplicate-delivery flag (§3.3.1.1).
	Dup bool

	// ContentType is the MQTT 5.0 Content Type property (0x03), empty when
	// absent.
	ContentType string
	// ResponseTopic is the MQTT 5.0 Response Topic property (0x08) for
	// request/response, empty when absent.
	ResponseTopic string
	// CorrelationData is the MQTT 5.0 Correlation Data property (0x09),
	// nil when absent.
	CorrelationData []byte
	// MessageExpirySeconds is the MQTT 5.0 Message Expiry Interval
	// property (0x02), zero when absent.
	MessageExpirySeconds uint32
	// PayloadFormatUTF8 reports the MQTT 5.0 Payload Format Indicator
	// property (0x01): true when the publisher declared the payload a
	// UTF-8 string.
	PayloadFormatUTF8 bool
	// SubscriptionIdentifiers holds the MQTT 5.0 Subscription Identifier
	// property values (0x0B) the broker attached, one per matching
	// subscription that carried an identifier. Nil when absent.
	SubscriptionIdentifiers []uint32
	// UserProperties holds the MQTT 5.0 User Property pairs (0x26) in the
	// order the publisher sent them. Nil when absent.
	UserProperties []UserProperty
}

Message is an inbound application message delivered to a MessageHandler. Beyond the MQTT 3.1.1 fields (Topic, Payload, QoS, Retain, Dup) it carries the MQTT 5.0 PUBLISH properties the broker set; on an MQTT 3.1.1 link every v5-only field is zero.

type MessageHandler

type MessageHandler func(msg *Message)

MessageHandler is invoked for every message a subscription receives.

The Message.Retain flag carries the MQTT PUBLISH "retain" bit (§3.3.1.3) so a handler that performs side effects on inbound commands can drop retained replays from the broker. On every (re)connect the broker re-delivers the last retained message on each subscribed filter; without this flag a handler cannot tell that replay apart from a fresh command, so an old `mosquitto_pub -r` left over from a previous test or automation is re-applied to the real device every time the consumer restarts. Consumers that don't care (pure state sinks) can simply ignore the flag.

Contract: the TCPClient adapter calls the handler synchronously, inline in its read loop, which is the same goroutine that decodes PUBACK/PUBREC/PUBCOMP and PINGRESP frames and feeds the keep-alive watchdog. The handler MUST return quickly and must not block on I/O, locks, or anything else of unbounded duration. If the work takes real time, hand it off to a queue or a goroutine instead of doing it inline — otherwise acknowledgement and PINGRESP processing stalls behind it and the keep-alive watchdog can declare the connection lost (spurious `ping_timeout`) even though the broker and network are fine. The *Message and its slices are owned by the caller for the duration of the call only; a handler that retains them past return must copy.

func LegacyHandler added in v1.0.0

func LegacyHandler(fn func(topic string, payload []byte, retained bool)) MessageHandler

LegacyHandler adapts a v0.x-style handler — the func(topic string, payload []byte, retained bool) shape used before the v1.0 Message type — into a MessageHandler. It lets consumers migrate call sites incrementally without rewriting every handler at once.

type ProtocolVersion added in v1.0.0

type ProtocolVersion = protocol.Version

ProtocolVersion is the MQTT protocol level a client speaks. It aliases protocol.Version so callers configure the wire dialect without importing the codec package directly.

const (
	// ProtocolV311 selects MQTT 3.1.1 (protocol level 4).
	ProtocolV311 ProtocolVersion = protocol.V311
	// ProtocolV50 selects MQTT 5.0 (protocol level 5) and is the default.
	ProtocolV50 ProtocolVersion = protocol.V50
)

Supported protocol versions.

type PublishOption added in v1.0.0

type PublishOption func(*publishOptions)

PublishOption attaches an MQTT 5.0 PUBLISH property to a single Publisher.Publish call. Options have no effect on an MQTT 3.1.1 link (that dialect has no property block). Apply them variadically:

c.Publish(ctx, "t", p, mqtt.QoS1, false,
	mqtt.WithContentType("application/json"),
	mqtt.WithMessageExpiry(30))

func WithContentType added in v1.0.0

func WithContentType(ct string) PublishOption

WithContentType sets the MQTT 5.0 Content Type property (0x03), a UTF-8 description of the payload's content (e.g. a MIME type).

func WithCorrelationData added in v1.0.0

func WithCorrelationData(data []byte) PublishOption

WithCorrelationData sets the MQTT 5.0 Correlation Data property (0x09) a responder echoes back so a requester can match a reply to its request.

func WithMessageExpiry added in v1.0.0

func WithMessageExpiry(seconds uint32) PublishOption

WithMessageExpiry sets the MQTT 5.0 Message Expiry Interval property (0x02): the broker discards the message for any subscriber it has not been delivered to within seconds.

func WithPayloadFormatUTF8 added in v1.0.0

func WithPayloadFormatUTF8() PublishOption

WithPayloadFormatUTF8 sets the MQTT 5.0 Payload Format Indicator property (0x01) to "UTF-8 encoded character data", declaring the payload a valid UTF-8 string.

func WithResponseTopic added in v1.0.0

func WithResponseTopic(topic string) PublishOption

WithResponseTopic sets the MQTT 5.0 Response Topic property (0x08) used in the request/response pattern to tell the receiver where to reply.

func WithUserProperties added in v1.0.0

func WithUserProperties(props ...UserProperty) PublishOption

WithUserProperties appends MQTT 5.0 User Property pairs (0x26) to the PUBLISH. Repeated calls accumulate; order is preserved on the wire.

type Publisher

type Publisher interface {
	// Publish sends payload to topic at the given QoS. QoS 0 is
	// fire-and-forget; QoS 1 blocks until PUBACK; QoS 2 blocks until
	// PUBCOMP. The variadic options attach MQTT 5.0 PUBLISH properties
	// (ignored on an MQTT 3.1.1 link).
	Publish(ctx context.Context, topic string, payload []byte, qos QoS, retain bool, opts ...PublishOption) error
}

Publisher is the outbound contract the bridge publishes through. Adapters wrap any MQTT client.

type QoS

type QoS byte

QoS mirrors the MQTT Quality of Service enum (§4.3): at-most-once (0), at-least-once (1) and exactly-once (2).

const (
	QoS0 QoS = 0
	QoS1 QoS = 1
	QoS2 QoS = 2
)

QoS values.

type ReasonCode added in v1.0.0

type ReasonCode = protocol.ReasonCode

ReasonCode is a single-byte MQTT 5.0 reason code (§2.4), surfaced on results and on ReasonError. It aliases protocol.ReasonCode. Values >= 0x80 denote failure (see protocol.ReasonCode.IsError).

type ReasonError added in v1.0.0

type ReasonError struct {
	// Packet names the control packet the broker rejected, e.g.
	// "PUBLISH", "SUBSCRIBE".
	Packet string
	// Code is the reason code the broker returned (>= 0x80).
	Code ReasonCode
	// Reason is the optional human-readable Reason String property
	// (0x1F) the broker attached, empty when absent.
	Reason string
}

ReasonError reports a broker-supplied MQTT 5.0 failure reason code (§2.4) on a packet the client sent. It is returned by Publish for a PUBACK/PUBREC carrying a code >= 0x80, and by Subscribe for a SUBACK grant that is a failure code. Match it with errors.As:

var re *mqtt.ReasonError
if errors.As(err, &re) && re.Code == mqtt.NotAuthorized { ... }

func (*ReasonError) Error added in v1.0.0

func (e *ReasonError) Error() string

Error implements the error interface.

type RetainHandling added in v1.0.0

type RetainHandling byte

RetainHandling selects how a broker delivers retained messages for a filter at subscribe time (MQTT 5.0 §3.8.3.1).

const (
	// SendRetained delivers retained messages when the subscription is
	// established (the MQTT 3.1.1 behaviour, and the default).
	SendRetained RetainHandling = 0
	// SendRetainedIfNew delivers retained messages only if the
	// subscription did not already exist.
	SendRetainedIfNew RetainHandling = 1
	// DontSendRetained never delivers retained messages at subscribe
	// time.
	DontSendRetained RetainHandling = 2
)

Retain-handling options for WithRetainHandling.

type SessionStore added in v1.0.0

type SessionStore interface {
	// Save inserts or replaces the entry keyed by (m.ID, m.Kind). The
	// store owns the sequence number: a new key is assigned the next
	// monotonic Seq, an existing key keeps its original Seq so an update
	// (e.g. a DUP re-save) does not jump the entry to the back of the
	// replay order.
	Save(m StoredMessage) error
	// Delete removes the entry keyed by (id, kind). Deleting an absent key
	// is a no-op.
	Delete(id uint16, kind StoredKind) error
	// All returns every stored entry ordered by ascending Seq — i.e. in
	// the order entries were first saved, which is the order they must be
	// replayed on a resumed session.
	All() ([]StoredMessage, error)
	// Reset discards all entries and restarts the sequence counter. Called
	// when the broker reports Session Present = 0, or on a clean start.
	Reset() error
}

SessionStore persists the QoS>0 flow-control state of a single MQTT session so an in-flight exchange survives a reconnect that resumes the session (CONNACK Session Present = 1). Implementations MUST be safe for concurrent use: the read loop, the keep-alive loop and application Publish calls all touch the store from different goroutines.

type StoredKind added in v1.0.0

type StoredKind uint8

StoredKind classifies a StoredMessage held in a SessionStore. It distinguishes the three QoS>0 flow-control artefacts the client must persist across a reconnect to complete an in-flight exchange.

const (
	// StoredPublish is an outbound QoS 1 or QoS 2 PUBLISH that has been
	// sent but not yet fully acknowledged (awaiting PUBACK, or PUBREC
	// before the PUBREL leg). On a resumed session it is replayed with the
	// DUP flag set.
	StoredPublish StoredKind = iota + 1
	// StoredPubrel is the PUBREL leg of an outbound QoS 2 exchange: the
	// PUBREC has been received, so the original PUBLISH is superseded and
	// only the PUBREL must be resent until PUBCOMP arrives.
	StoredPubrel
	// StoredInboundID records an inbound QoS 2 packet identifier that has
	// been delivered to the application and PUBREC'd, pending the peer's
	// PUBREL (exactly-once receiver state, method A).
	StoredInboundID
)

Stored message kinds.

func (StoredKind) String added in v1.0.0

func (k StoredKind) String() string

String returns a short lower-case label for the kind, suitable for structured log fields.

type StoredMessage added in v1.0.0

type StoredMessage struct {
	Publish *protocol.PublishPacket
	Seq     uint64
	ID      uint16
	Kind    StoredKind
}

StoredMessage is one entry of persisted QoS>0 session state. ID is the MQTT packet identifier the entry is keyed by (together with Kind); Seq is a store-assigned monotonic sequence that fixes replay order; Publish is the packet to (re)transmit for a StoredPublish entry and is nil for the identifier-only kinds.

type SubscribeOption added in v1.0.0

type SubscribeOption func(*subscribeOptions)

SubscribeOption sets an MQTT 5.0 subscription option on a single Subscriber.Subscribe call. Options have no effect on an MQTT 3.1.1 link (that dialect carries only QoS in the options byte).

func WithNoLocal added in v1.0.0

func WithNoLocal() SubscribeOption

WithNoLocal sets the MQTT 5.0 No Local option: the broker does not forward a message back to the connection that published it.

func WithRetainAsPublished added in v1.0.0

func WithRetainAsPublished() SubscribeOption

WithRetainAsPublished sets the MQTT 5.0 Retain As Published option: the broker keeps the original PUBLISH retain flag instead of clearing it when forwarding to this subscription.

func WithRetainHandling added in v1.0.0

func WithRetainHandling(h RetainHandling) SubscribeOption

WithRetainHandling sets the MQTT 5.0 Retain Handling option controlling whether retained messages are delivered at subscribe time.

type SubscribeResult added in v1.0.0

type SubscribeResult struct {
	// GrantedQoS is the maximum QoS the broker granted for the filter.
	GrantedQoS QoS
	// ReasonCode is the SUBACK reason code for the filter (a success
	// grant; failure codes are returned as a *[ReasonError] instead).
	ReasonCode ReasonCode
}

SubscribeResult is the outcome of a Subscriber.Subscribe call, decoded from the SUBACK. A non-error result may still carry a GrantedQoS below the requested one (the broker is permitted to downgrade).

type Subscriber

type Subscriber interface {
	// Subscribe registers handler for filter at the requested QoS and
	// blocks until the SUBACK arrives (bounded by ctx and the configured
	// ack timeout). A granted QoS below the requested one is not an error;
	// a broker rejection (reason code >= 0x80) yields a *[ReasonError].
	// The variadic options set MQTT 5.0 subscription options.
	Subscribe(ctx context.Context, filter string, qos QoS, handler MessageHandler, opts ...SubscribeOption) (SubscribeResult, error)
	// Unsubscribe removes the subscription for filter.
	Unsubscribe(ctx context.Context, filter string) error
}

Subscriber is the inbound contract — subscribe to a topic filter and route matching messages to a handler. Wiring typically happens once at startup and stays active for the broker connection's lifetime.

type TCPClient

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

TCPClient is a reconnecting MQTT 3.1.1 / 5.0 client over TCP or TLS. It implements both Client (Publish + Subscribe/Unsubscribe) and Connector (Connect + Disconnect) so a Lifecycle can drive its full lifecycle, plus ConnectionNotifier for event-driven reconnects.

Lock ordering. The client holds several independent short-lived locks; to stay deadlock-free they are always acquired in this order and no lock is ever held across a blocking channel receive or a socket write:

connMu → quota.mu → ids.mu → store.mu → waitersMu → link.sendMu

connMu is outermost and long-lived by design: it serialises Connect and Disconnect end-to-end (including the dial and the CONNACK round-trip) so two concurrent Connect calls can never establish two live links sharing one session state. It is never taken by the read/keep-alive loops or the publish path. In practice the remaining critical sections do not nest: the read loop signals a waiter (waitersMu) and then, separately, writes a reply (sendMu); the publish path acquires quota, then an id, saves to the store, registers a waiter, and only then writes. link.sendMu is a leaf — nothing else is acquired while it is held. subsMu (the subscription slice) is independent of all the above; the read loop copies the matching handlers out from under it before invoking them.

func NewTCPClient

func NewTCPClient(cfg TCPConfig) *TCPClient

NewTCPClient constructs a client from cfg, applying defaults for any unset timing/limit field. It does not dial; call TCPClient.Connect.

func (*TCPClient) Connect

func (c *TCPClient) Connect(ctx context.Context) error

Connect implements Connector. It dials, sends CONNECT, waits for CONNACK, applies the negotiated limits, replays any resumable session state and prior subscriptions, and starts the read and keep-alive loops. A live link makes it a no-op that returns a wrapped ErrAlreadyConnected so Lifecycle treats it as idempotent. Connect and Disconnect are serialised against each other (connMu), so concurrent calls cannot establish two live links sharing one session state.

func (*TCPClient) ConnectResult added in v1.0.0

func (c *TCPClient) ConnectResult() (ConnectResult, bool)

ConnectResult returns the negotiated session state from the most recent successful connect. The bool is false before the first connect.

func (*TCPClient) ConnectionLost

func (c *TCPClient) ConnectionLost() <-chan struct{}

ConnectionLost returns a channel that receives a value whenever the client detects its connection dropped. It is buffered (size 1) so a drop is never missed by a lifecycle loop that reacts to it.

func (*TCPClient) Disconnect

func (c *TCPClient) Disconnect(ctx context.Context) error

Disconnect implements Connector. It sends a best-effort DISCONNECT, tears the link down gracefully (no connection-lost signal), and waits — bounded by ctx — for the read and keep-alive loops to exit. It is serialised against Connect (connMu), so it can no longer report success while a concurrent Connect is mid-handshake and about to establish a fresh link.

func (*TCPClient) IsConnected

func (c *TCPClient) IsConnected() bool

IsConnected reports whether the client currently holds a link.

func (*TCPClient) LastConnectedAt

func (c *TCPClient) LastConnectedAt() time.Time

LastConnectedAt returns the wall-clock instant of the most recent successful CONNECT, or the zero time when none has happened.

func (*TCPClient) Publish

func (c *TCPClient) Publish(ctx context.Context, topic string, payload []byte, qos QoS, retain bool, opts ...PublishOption) error

Publish implements Publisher. QoS 0 is fire-and-forget; QoS 1 blocks until PUBACK; QoS 2 blocks until PUBCOMP. A disconnected client fails fast with ErrNotConnected; a connection lost mid-flight fails with ErrConnectionLost; a broker failure reason code yields a *ReasonError. Each acknowledgement wait is bounded by ctx and the configured AckTimeout.

func (*TCPClient) Subscribe

func (c *TCPClient) Subscribe(ctx context.Context, filter string, qos QoS, handler MessageHandler, opts ...SubscribeOption) (SubscribeResult, error)

Subscribe implements Subscriber. It sends a single-filter SUBSCRIBE and blocks until the SUBACK (bounded by ctx and AckTimeout). A granted QoS below the requested one is not an error; a rejection (reason >= 0x80) yields a *ReasonError. On success the handler is registered so it survives reconnects (resubscribe replay).

func (*TCPClient) Unsubscribe

func (c *TCPClient) Unsubscribe(ctx context.Context, filter string) error

Unsubscribe implements Subscriber. It sends a single-filter UNSUBSCRIBE, blocks until the UNSUBACK, and removes the local handler registration — unless a concurrent Subscribe for the same filter registered a newer handler while the UNSUBSCRIBE was in flight; that registration is left intact (the broker processed the later SUBSCRIBE after this UNSUBSCRIBE, so its subscription is live and removing the handler would silently drop its messages).

type TCPConfig

type TCPConfig struct {
	// BrokerURL is the broker endpoint: tcp://host[:1883] or
	// tls://host[:8883] (mqtt://, ssl://, mqtts:// are accepted aliases).
	BrokerURL string
	// ClientID is the MQTT client identifier. An empty identifier on an
	// MQTT 5.0 link asks the broker to assign one (surfaced on
	// [ConnectResult.AssignedClientID]).
	ClientID string
	// Username is the optional CONNECT user name.
	Username string
	// Password is the optional CONNECT password. On an MQTT 3.1.1 link a
	// password without a username is rejected by the codec.
	Password string

	// ProtocolVersion selects the wire dialect. The zero value selects
	// MQTT 5.0 ([ProtocolV50]).
	ProtocolVersion ProtocolVersion
	// CleanStart is the CONNECT Clean Start bit (MQTT 3.1.1 Clean Session).
	// When true the broker discards any prior session and the client resets
	// its own stored QoS>0 state.
	CleanStart bool
	// SessionExpirySeconds is the MQTT 5.0 Session Expiry Interval (0x11)
	// requested in CONNECT. Zero requests a session that ends when the
	// connection closes (treated as a clean start for local session state).
	SessionExpirySeconds uint32
	// ReceiveMaximum is the number of unacknowledged QoS>0 PUBLISHes the
	// client will accept concurrently, advertised to the broker (MQTT 5.0
	// 0x21). Zero advertises nothing (the 65535 default).
	ReceiveMaximum uint16
	// MaximumPacketSize is both the largest packet the client will accept
	// (advertised in CONNECT, enforced by the read loop) and the read-frame
	// cap. Zero selects 1 MiB.
	MaximumPacketSize uint32
	// TopicAliasMaximum is the highest inbound topic alias the client
	// accepts, advertised to the broker (MQTT 5.0 0x22). Zero disables
	// inbound topic aliasing.
	TopicAliasMaximum uint16
	// UserProperties are MQTT 5.0 User Property pairs (0x26) attached to the
	// CONNECT.
	UserProperties []UserProperty

	// KeepAlive is the requested keep-alive interval; values below the 30s
	// floor are raised to it. A broker Server Keep Alive overrides the ping
	// schedule even below the floor.
	KeepAlive time.Duration
	// DialTimeout bounds the dial and CONNECT/CONNACK round-trip (default
	// 10s).
	DialTimeout time.Duration
	// AckTimeout bounds each Publish/Subscribe acknowledgement wait (default
	// 20s).
	AckTimeout time.Duration
	// MaxInflight caps concurrent in-flight outbound QoS>0 sends on an MQTT
	// 3.1.1 link (which has no Receive Maximum). Zero selects 65535.
	MaxInflight uint16

	// TLSConfig is used for tls:// endpoints (tls://, ssl://, mqtts://).
	// When nil a minimal config verifying the broker hostname is built
	// automatically. A supplied config is cloned before use and, when its
	// ServerName is empty, the hostname from BrokerURL is filled in — so a
	// CA-pinning config (RootCAs only) verifies the broker identity instead
	// of failing the handshake. On a non-TLS scheme (tcp://, mqtt://) a
	// configured TLSConfig is NOT used; Connect logs a warning because the
	// connection proceeds in plaintext despite transport security having
	// been configured.
	TLSConfig *tls.Config
	// Will is the optional Last Will and Testament published by the broker
	// if the connection drops without a clean DISCONNECT.
	Will *Will
	// Logger receives structured diagnostics. Defaults to [slog.Default].
	Logger *slog.Logger
}

TCPConfig wires a TCPClient against a real broker. The zero value is not usable on its own — at least BrokerURL and ClientID must be set — but every timing/limit field has a sane default applied by NewTCPClient.

type UserProperty added in v1.0.0

type UserProperty = protocol.UserProperty

UserProperty is a single MQTT 5.0 User Property key/value pair. It aliases protocol.UserProperty. User Properties are repeatable and preserve order on both publish and receive.

type Will added in v1.0.0

type Will struct {
	// Topic is the will message topic.
	Topic string
	// ContentType is the will Content Type property (0x03).
	ContentType string
	// ResponseTopic is the will Response Topic property (0x08).
	ResponseTopic string
	// Payload is the will message payload.
	Payload []byte
	// CorrelationData is the will Correlation Data property (0x09).
	CorrelationData []byte
	// QoS is the will message QoS.
	QoS QoS
	// Retain sets the retain flag on the will message.
	Retain bool
	// PayloadFormatUTF8 sets the will Payload Format Indicator property
	// (0x01).
	PayloadFormatUTF8 bool
	// DelayIntervalSeconds is the MQTT 5.0 Will Delay Interval property
	// (0x18): the broker waits this long after the connection closes
	// before publishing the will.
	DelayIntervalSeconds uint32
	// MessageExpirySeconds is the will Message Expiry Interval property
	// (0x02).
	MessageExpirySeconds uint32
	// UserProperties holds MQTT 5.0 User Property pairs (0x26) attached to
	// the will message.
	UserProperties []UserProperty
}

Will is the client's Last Will and Testament: the message the broker publishes on the client's behalf if the connection closes without a clean DISCONNECT. The MQTT 5.0 fields (DelayIntervalSeconds and the property-backed values) are ignored on an MQTT 3.1.1 link.

Directories

Path Synopsis
e2e
gencert command
Package main implements gencert, a standalone generator for the throwaway CA and TLS server certificate the e2e test brokers (see ../testdata/mosquitto.conf) use for their TLS listener.
Package main implements gencert, a standalone generator for the throwaway CA and TLS server certificate the e2e test brokers (see ../testdata/mosquitto.conf) use for their TLS listener.
Package protocol implements the MQTT wire codec — packet encode/decode for both MQTT 3.1.1 (protocol level 4, V311) and MQTT 5.0 (protocol level 5, V50) — that the client library builds on.
Package protocol implements the MQTT wire codec — packet encode/decode for both MQTT 3.1.1 (protocol level 4, V311) and MQTT 5.0 (protocol level 5, V50) — that the client library builds on.

Jump to

Keyboard shortcuts

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