Repository navigation
Close codes and shutdown
- You want to close a connection with a code and a reason the other side can read, instead of dropping it with no explanation.
- You're deciding between closing one connection and shutting the whole server down.
- You want a client (Go or JavaScript) to tell a deliberate close apart from a dropped network, and react differently.
A websocket connection ends with a close frame: a numeric code plus an optional human-readable reason, sent by whichever side closes first. neffos exposes the code and reason on both sides, lets you choose them when closing from your own code, and reads them back out of any error with neffos.CloseStatus.
const (
CloseNormalClosure = 1000
CloseGoingAway = 1001
CloseProtocolError = 1002
CloseUnsupportedData = 1003
CloseNoStatusReceived = 1005
CloseAbnormalClosure = 1006
CloseInvalidFramePayloadData = 1007
ClosePolicyViolation = 1008
CloseMessageTooBig = 1009
CloseMandatoryExtension = 1010
CloseInternalServerErr = 1011
CloseServiceRestart = 1012
CloseTryAgainLater = 1013
CloseTLSHandshake = 1015
)These come from RFC 6455 and the IANA registry; neffos uses a handful of them itself (CloseGoingAway on shutdown, CloseMessageTooBig on an oversized message, ClosePolicyViolation as the default for a plain error returned from OnConnect). The range 4000 to 4999 is reserved for applications: pick any code in it for your own meanings (a ban, a kick, a rate limit), the way _examples/01-getting-started/09-close-and-shutdown uses 4000 for "kicked by operator".
From inside an event handler (or Server.OnConnect), return a neffos.CloseError instead of a plain error to choose the close code:
neffos.Events{
"chat": func(c *neffos.NSConn, msg neffos.Message) error {
if isBanned(c.Conn) {
return neffos.CloseError{Code: 4003, Reason: "banned"}
}
// ...
return nil
},
}type CloseError struct {
Code int
Reason string
}CloseError{Code: 1008} alone is a valid literal; Error() renders as "[Code] text", and Unwrap() exposes any underlying error so errors.Is/errors.As still see through it. A plain (non-CloseError) error returned from OnConnect closes with ClosePolicyViolation (1008); return a CloseError yourself to pick a different code.
From outside a handler, close a specific connection directly:
c.Terminate(4000, "kicked by operator")func (c *Conn) Terminate(code int, reason string)
func (c *Conn) Close() // Terminate(CloseNormalClosure, "")Close() is shorthand for a normal, no-reason close; Terminate is what you want whenever the code or the reason matters. Both are idempotent: a second call on an already-closed connection does nothing.
On the side that stayed open, Conn.Err() holds whatever closed the other one, and neffos.CloseStatus turns it into a number. A peer that drops with a clean FIN and never sends a close frame reports neffos.CloseAbnormalClosure (1006) on all three backends, so that case still has a code. CloseStatus returns -1 only when there was no code at all: a read or write timeout, a heartbeat failure, or a hard TCP reset.
// Server-side, in OnDisconnect:
srv.OnDisconnect = func(c *neffos.Conn) {
log.Printf("[%s] disconnected, close status %d", c.ID(), neffos.CloseStatus(c.Err()))
}// Client-side, once NotifyClose fires:
<-client.NotifyClose
code := neffos.CloseStatus(client.Conn().Err())Conn.Err() is set before NotifyClose fires, so reading it right after the channel closes is race-free. See _examples/01-getting-started/09-close-and-shutdown for both sides of this together: the server logging CloseStatus(c.Err()) in OnDisconnect, and the kicked client printing the code and reason it got from client.Conn().Err().
conn.onclose = () => {
console.log(conn.closeInfo.code, conn.closeInfo.reason);
};conn.closeInfo ({ code, reason, wasClean, error? }) describes how the socket last closed; it is undefined while the connection is open. A pending ask or an in-flight connect that the connection drops out from under rejects with ErrClosed, which neffos.isCloseError does not recognize by itself (it only matches a CloseError or the literal "[-1] write closed" text); check err === neffos.ErrClosed || neffos.isCloseError(err) to tell a close-triggered rejection apart from a domain error returned by a handler. See Errors for the rest of the error helpers.
func (s *Server) Close()
func (s *Server) Shutdown(ctx context.Context) errorBoth close every connection with CloseGoingAway (1001) and a reason ("server closed" or "server shutting down"), at the same time, not one by one. The difference is what they wait for:
-
Close()waits for each connection'sOnNamespaceDisconnect/OnRoomLeavecallbacks and the server's ownOnDisconnectto run, then signals the dispatch loop to stop and releases theStackExchange(if one implementsStackExchangeCloser). It does not wait for any event callback still running elsewhere. -
Shutdown(ctx)does everythingClosedoes, and then also waits for every connection's reader goroutine, including any event callback it is in the middle of running, untilctxis done. It returnsctx.Err()if the deadline passes first, otherwisenil. Call it with a bounded context:context.WithTimeout(context.Background(), 5*time.Second)is what the examples use.
Neither should be called from inside an event callback, OnConnect, or OnDisconnect: that callback is one of the things Shutdown is waiting for, so calling it from there waits on itself.
The usual shape, from _examples/01-getting-started/09-close-and-shutdown, pairs signal.NotifyContext with both Server.Shutdown and the standard library's http.Server.Shutdown, in that order:
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
// ... start the HTTP server in a goroutine ...
<-ctx.Done()
stop()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("websocket shutdown: %v", err)
}
if err := httpServer.Shutdown(ctx); err != nil {
log.Printf("http shutdown: %v", err)
}http.Server.Shutdown goes after, not before: it does not track a hijacked websocket connection as one of its own, so it would return immediately without having closed a single neffos socket if called first.
The close code a client receives is also the input to whether it should try reconnecting at all. On the JavaScript client, ReconnectOptions.shouldReconnect(info) gets the same CloseInfo described above and decides:
reconnect: {
shouldReconnect: (info) => info.code !== 4001, // 4001: don't come back
}Server.Shutdown/Server.Close using CloseGoingAway (1001) specifically is what makes this useful: a shouldReconnect that checks info.code can tell "the server is shutting down on purpose" apart from a dropped network, and choose not to reconnect into a server that is going away anyway (or, just as validly, choose to keep retrying because a restart is expected). See Reconnection for the full reconnect lifecycle, defaults and backoff.
_examples/01-getting-started/09-close-and-shutdown: kick <name> closes one connection with code 4000, Ctrl+C runs Server.Shutdown, and both sides read the resulting code back with CloseStatus.
-
Errors,
IsCloseError/IsDisconnectError/IsTimeoutErrorand the known-error registry - Reconnection, what a client does after a close, and when it decides not to
- Timeouts, the heartbeat and size-limit closes that land here too
- Architecture, where the close handshake fits in the connection lifecycle
Home | About | Project | Getting Started | Technical Docs | Copyright © 2019-2026 Gerasimos Maropoulos. Documentation terms of use.
Getting started
Concepts
Messaging
Production
Scale out