Skip to content

listeners: bind the Websocket listener at Init and report the bound address - #542

Open
ChristophNetsch wants to merge 2 commits into
mochi-mqtt:mainfrom
alpamayo-solutions:fix/websocket-binds-at-init
Open

ChristophNetsch wants to merge 2 commits into
mochi-mqtt:mainfrom
alpamayo-solutions:fix/websocket-binds-at-init

Conversation

@ChristophNetsch

Copy link
Copy Markdown

Problem

listeners.Websocket builds its http.Server in Init but binds only inside Serve (ListenAndServe / ListenAndServeTLS), so Address() echoes the configured string. Two consequences:

  1. A ":0" address can never be reported — the caller learns no port.
  2. The port is not held between Init and Serve. A caller that works around (1) by pre-resolving ":0" (bind, read the port, release, hand the concrete address to NewWebsocket) can see another listener in the same process take the port in between. Serve then fails with address already in use, and the address the caller already advertised is served by something else. We hit exactly this in our CI: a wss dial landed on an unrelated HTTPS server that had been given the released port, and failed with websocket: bad handshake.

The TCP listener already does the right thing: it binds in Init and Address() returns listen.Addr().String().

Change

Websocket binds in Init like TCP, serves that socket (Serve(ln) / ServeTLS(ln, "", "")), reports the bound address from Address(), releases the socket on Close even when Serve never ran (http.Server.Shutdown only closes listeners Serve registered), and refuses an unbindable address at Init where the caller can see the error.

Behavioural differences a caller can observe:

  • Init now returns a bind error for an unbindable address (TestWebsocketFailedToServe adapted: the error surfaces at Init; Serve on an unbound listener logs and returns instead of panicking).
  • Address() after Init returns the bound address (e.g. [::]:1882 for a config of :1882), consistent with the TCP listener (TestServerAddListenersFromConfig adapted).

New test: TestWebsocketAddressIsTheBoundPortAfterInit — a ":0" listener reports a real port after Init, accepts a TCP dial before Serve, and a second net.Listen on that port fails.

Christoph Netsch added 2 commits September 3, 2026 01:42
…Messages

RetainMessage writes particle.retainPath under the root and particle locks;
scanMessages (a subscriber's retained-message scan, via Messages) read it
with no lock at all. A retained publish racing a wildcard SUBSCRIBE is a
data race the race detector reports as:

  Write at ... by (*TopicsIndex).RetainMessage  topics.go:461
  Previous read at ... by (*TopicsIndex).scanMessages  topics.go:554

This is mochi-mqtt#200, closed as not reproducible; the added test reproduces it
deterministically under -race and passes with the two reads locked.
The TCP listener binds in Init and Address() reports the socket's own
address; the Websocket listener built its http.Server in Init and bound
only inside Serve (ListenAndServe), so Address() echoed the configured
string. Two consequences: a ":0" address could never be reported, and the
port was not held between Init and Serve — a caller that pre-resolved ":0"
by binding and releasing could see another listener take the port in
between, after which Serve failed with "address already in use" and the
address it had advertised was served by something else.

Bind in Init like the TCP listener, serve that socket (Serve / ServeTLS),
report the bound address, release the socket on Close even when Serve never
ran, and refuse an unbindable address at Init where the caller can see it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant