Extra guides
WebSockets
Upgrade HTTP requests and exchange bidirectional browser messages with lazywebsocket.
Use WebSockets when the browser and server both need to send messages over one long-lived connection. For one-way server-to-browser updates, start with Server-Sent Events. For request-response HTML updates, start with Turbo Frames or Server-Rendered Updates.
Register an endpoint
WebSocket endpoints are plain HTTP handlers because the protocol starts as an HTTP request and then upgrades the connection:
package appinit
import (
"net/http"
"golazy.dev/lazyroutes"
"myapp/app/realtime"
)
func Draw(router *lazyroutes.Scope) {
router.HandleFunc(http.MethodGet, "/chat/socket", realtime.ChatSocket)
}
Keep the route separate from controller actions that render HTML. Once the connection is upgraded, normal response buffering, view rendering, and Turbo frame rendering no longer apply.
Upgrade the request
Use golazy.dev/lazywebsocket from the handler:
package realtime
import (
"fmt"
"net/http"
"golazy.dev/lazycontroller"
"golazy.dev/lazywebsocket"
)
var chatUpgrader = lazywebsocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
func ChatSocket(w http.ResponseWriter, r *http.Request) error {
if !lazywebsocket.IsWebSocketUpgrade(r) {
return lazycontroller.Error(
http.StatusBadRequest,
fmt.Errorf("websocket upgrade required"),
)
}
conn, err := chatUpgrader.Upgrade(w, r, nil)
if err != nil {
return nil
}
defer conn.Close()
for {
messageType, payload, err := conn.ReadMessage()
if err != nil {
return nil
}
if messageType != lazywebsocket.TextMessage {
continue
}
if err := conn.WriteMessage(lazywebsocket.TextMessage, payload); err != nil {
return nil
}
}
}
Upgrade writes the HTTP handshake response. If it fails, return nil after
the upgrader has handled the response.
Connect from the browser
Choose the WebSocket scheme from the current page:
const scheme = location.protocol === "https:" ? "wss:" : "ws:"
const socket = new WebSocket(`${scheme}//${location.host}/chat/socket`)
socket.addEventListener("message", (event) => {
console.log("socket message", event.data)
})
socket.addEventListener("open", () => {
socket.send("hello")
})
When the app runs behind a reverse proxy, the proxy must pass the Upgrade and
Connection headers through to the GoLazy app process.
Guard the origin
Browsers can try to open WebSockets across origins. lazywebsocket.Upgrader
uses a safe default when CheckOrigin is nil: if an Origin header is present,
the origin host must match the request host. Set CheckOrigin only when the app
intentionally allows a different browser origin:
var chatUpgrader = lazywebsocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return r.Header.Get("Origin") == "https://admin.example.com"
},
}
Avoid the deprecated package-level lazywebsocket.Upgrade helper for browser
routes because it does not perform origin checking.
Keep reads and writes disciplined
A WebSocket connection supports one concurrent reader and one concurrent writer. If the app has independent inbound and outbound loops, keep all writes behind one goroutine or one channel-owned writer.
Read from the connection even when the app mostly sends messages. Reads process close, ping, and pong control frames. If an unexpected close matters for debugging, classify it explicitly:
if lazywebsocket.IsUnexpectedCloseError(
err,
lazywebsocket.CloseGoingAway,
lazywebsocket.CloseNormalClosure,
) {
return fmt.Errorf("read chat socket: %w", err)
}
Use application services for fanout, authorization, persistence, and replay. The WebSocket handler should own the connection lifecycle and delegate domain work to those services.