Documentation
¶
Overview ¶
Package websocket provides Credo's server-side WebSocket adapter over the exact-pinned github.com/coder/websocket protocol engine.
Create one Server with Use, then register Server.Handler through the normal Credo GET route API. Global, group, route, authentication, rewrite, and access-log middleware retain their normal ordering. Use integrates with the App's pre-infrastructure drain; applications that use an App only as an http.Handler must coordinate Server.Shutdown themselves.
The zero Config is secure and bounded: browser same-origin authorization, optional subprotocol negotiation, disabled compression, and a 32 KiB per-message read limit. Origin checks protect browser handshakes from cross-site abuse, but they are not authentication; applications must still authenticate and authorize connections.
A Conn is borrowed for the synchronous lifetime of a Handler. Its Context is the connection-lifetime context and should be used for WebSocket I/O instead of the HTTP request context. Neither the pooled *credo.Context nor Conn may be retained after the Handler returns. Every connection needs an active Read or CloseRead so control frames are processed.
Example ¶
package main
import (
"context"
"fmt"
"net/http/httptest"
"strings"
coderwebsocket "github.com/coder/websocket"
"github.com/credo-go/credo"
credows "github.com/credo-go/credo/websocket"
)
func main() {
app, err := credo.New(credo.WithoutAccessLog())
if err != nil {
panic(err)
}
ws := credows.Use(app, credows.Config{
Subprotocols: []string{"echo.v1"},
RequireSubprotocol: true,
})
app.GET("/echo", ws.Handler(func(_ *credo.Context, conn *credows.Conn) error {
typ, payload, readErr := conn.Read(conn.Context())
if readErr != nil {
return readErr
}
return conn.Write(conn.Context(), typ, payload)
}))
httpServer := httptest.NewServer(app)
defer httpServer.Close()
client, _, err := coderwebsocket.Dial( //nolint:bodyclose // Dial owns the response body.
context.Background(),
"ws"+strings.TrimPrefix(httpServer.URL, "http")+"/echo",
&coderwebsocket.DialOptions{Subprotocols: []string{"echo.v1"}},
)
if err != nil {
panic(err)
}
defer func() { _ = client.CloseNow() }()
if writeErr := client.Write(context.Background(), coderwebsocket.MessageText, []byte("hello")); writeErr != nil {
panic(writeErr)
}
_, payload, err := client.Read(context.Background())
if err != nil {
panic(err)
}
fmt.Println(string(payload))
}
Output: hello
Index ¶
- type CloseError
- type CompressionMode
- type Config
- type Conn
- func (c *Conn) Close(code StatusCode, reason string) error
- func (c *Conn) CloseRead(ctx context.Context) context.Context
- func (c *Conn) Context() context.Context
- func (c *Conn) Ping(ctx context.Context) error
- func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error)
- func (c *Conn) Subprotocol() string
- func (c *Conn) Unwrap() *coderwebsocket.Conn
- func (c *Conn) Write(ctx context.Context, typ MessageType, data []byte) error
- type Handler
- type MessageType
- type Server
- type StatusCode
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type CloseError ¶
type CloseError struct {
Code StatusCode
Reason string
}
CloseError reports a WebSocket close status received from the peer. Reason is untrusted peer input and must not be logged or displayed without sanitizing. CloseError deliberately does not unwrap an upstream implementation error.
func (CloseError) Error ¶
func (e CloseError) Error() string
Error returns a human-readable close diagnostic. Its text is not a parsing contract; use Code, Reason, or CloseStatus for programmatic inspection.
type CompressionMode ¶
type CompressionMode int
CompressionMode controls per-message deflate negotiation.
const ( // CompressionDisabled disables per-message compression. This is the default. CompressionDisabled CompressionMode = iota // CompressionNoContextTakeover resets compression state between messages. CompressionNoContextTakeover // CompressionContextTakeover reuses compression state between messages. CompressionContextTakeover )
type Config ¶
type Config struct {
// AllowedOrigins adds exact or one-left-label wildcard browser origins to
// the same-origin default, for example "https://app.example.com" or
// "https://*.example.com".
AllowedOrigins []string
// Subprotocols lists supported protocols in server preference order.
Subprotocols []string
// RequireSubprotocol rejects clients that do not offer a subprotocol.
RequireSubprotocol bool
// ReadLimit is the maximum bytes in one message. Zero uses 32 KiB; negative
// values are invalid. There is no public unlimited setting.
ReadLimit int64
// CompressionMode controls per-message deflate negotiation.
CompressionMode CompressionMode
// CompressionThreshold is the minimum message size to compress. Zero uses
// the selected mode's default; it is ignored when compression is disabled.
CompressionThreshold int
// InsecureSkipOriginCheck disables browser origin authorization. It is a
// CSRF footgun and cannot be combined with AllowedOrigins.
InsecureSkipOriginCheck bool
}
Config configures a WebSocket Server. Its zero value is secure: browser origins must be same-origin, compression is disabled, subprotocols are optional, and each message is limited to 32 KiB.
Example ¶
package main
import (
"context"
"github.com/coder/websocket"
"github.com/credo-go/credo"
credows "github.com/credo-go/credo/websocket"
)
type publicConnSurface interface {
Context() context.Context
Read(context.Context) (credows.MessageType, []byte, error)
Write(context.Context, credows.MessageType, []byte) error
Ping(context.Context) error
CloseRead(context.Context) context.Context
Close(credows.StatusCode, string) error
Subprotocol() string
Unwrap() *websocket.Conn
}
type publicServerSurface interface {
Handler(credows.Handler) credo.Handler
Shutdown(context.Context) error
}
var (
_ publicConnSurface = (*credows.Conn)(nil)
_ publicServerSurface = (*credows.Server)(nil)
_ error = credows.CloseError{}
_ credows.Handler = func(*credo.Context, *credows.Conn) error { return nil }
_ func(*credo.App, ...credows.Config) *credows.Server = credows.Use
)
func main() {
_ = credows.Config{
AllowedOrigins: []string{"https://app.example.com"},
Subprotocols: []string{"events.v1"},
RequireSubprotocol: true,
ReadLimit: 1 << 20,
CompressionMode: credows.CompressionNoContextTakeover,
}
}
Output:
type Conn ¶
type Conn struct {
// contains filtered or unexported fields
}
Conn is a Credo-owned façade over an accepted WebSocket connection.
Methods may be called concurrently except that only one Read may be active at a time. CloseRead claims the read side permanently; Read must not be called after it. Concurrent Write calls are serialized by the protocol engine.
func (*Conn) Close ¶
func (c *Conn) Close(code StatusCode, reason string) error
Close performs the WebSocket close handshake. It rejects codes that a server cannot send, invalid UTF-8 reasons, and reasons longer than 123 bytes before writing a frame.
func (*Conn) CloseRead ¶
CloseRead starts processing control frames when the application does not expect more data messages. It permanently claims the read side. The returned context is cancelled when the upstream reader stops; unexpected data closes the connection with StatusPolicyViolation. Its cancellation can be delayed by the upstream protocol engine's bounded close guard.
func (*Conn) Context ¶
Context returns the connection-lifetime context. It is independent of the HTTP request cancellation and is cancelled when the adapter finishes the connection lifecycle.
func (*Conn) Read ¶
Read reads one text or binary message. Only one Read may be active at a time. Peer close errors are normalized to Credo CloseError values.
func (*Conn) Subprotocol ¶
Subprotocol returns the negotiated subprotocol, or an empty string when no subprotocol was negotiated.
func (*Conn) Unwrap ¶
func (c *Conn) Unwrap() *coderwebsocket.Conn
Unwrap returns the underlying coder/websocket connection as an expert escape hatch. Ownership is not transferred: handler-return cleanup, connection context cancellation, and lifecycle tracking still apply. Raw calls bypass Credo's message validation, error normalization, logging, and close policy; the returned connection must not outlive the Handler.
type Handler ¶
Handler handles an accepted WebSocket connection. The handler runs synchronously for the connection lifetime; req and conn must not be retained after it returns.
type MessageType ¶
type MessageType int
MessageType identifies a WebSocket data message type.
const ( // MessageText identifies a UTF-8 text message. MessageText MessageType = iota + 1 // MessageBinary identifies a binary message. MessageBinary )
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server owns the immutable policy and managed lifecycle state for WebSocket handlers registered through one Credo application. A Server must be created with Use.
func Use ¶
Use validates and freezes a WebSocket configuration for app. It accepts zero or one Config value and performs no I/O or DI publication. Invalid configuration, a nil app, or registration after the app is frozen panics as startup misuse.
func (*Server) Handler ¶
Handler adapts h into a Credo route handler. It panics for a nil handler. The application handler runs synchronously for the full connection lifetime.
func (*Server) Shutdown ¶
Shutdown stops admitting connections, sends active peers a Going Away close, and waits for every synchronous Handler and adapter cleanup to return. The first caller owns the global drain budget. A concurrent caller waits for that result unless its own context ends first; it never changes the owner's budget. Calls made after the owner finishes return its stable result.
If the owner context is cancelled or reaches its deadline before cleanup finishes, Shutdown reports an incomplete drain and the server remains draining until late handlers return. If cleanup finishes but a tracked close operation failed, the server is closed and Shutdown returns that error. A nil ctx is rejected without starting the drain.
type StatusCode ¶
type StatusCode int
StatusCode is a WebSocket close status code.
const ( // StatusNormalClosure indicates that the connection fulfilled its purpose. StatusNormalClosure StatusCode = 1000 // StatusGoingAway indicates that an endpoint is going away. StatusGoingAway StatusCode = 1001 // StatusProtocolError indicates a protocol error. StatusProtocolError StatusCode = 1002 // StatusUnsupportedData indicates an unsupported data type. StatusUnsupportedData StatusCode = 1003 // StatusNoStatusReceived represents a close frame without a status code. // It cannot be sent on the wire. StatusNoStatusReceived StatusCode = 1005 // StatusAbnormalClosure represents an abnormal close without a close frame. // It cannot be sent on the wire. StatusAbnormalClosure StatusCode = 1006 // StatusInvalidFramePayloadData indicates invalid frame payload data. StatusInvalidFramePayloadData StatusCode = 1007 // StatusPolicyViolation indicates an application policy violation. StatusPolicyViolation StatusCode = 1008 // StatusMessageTooBig indicates that a message exceeded an endpoint limit. StatusMessageTooBig StatusCode = 1009 // StatusMandatoryExtension indicates a client-required extension was absent. // A server cannot send this status. StatusMandatoryExtension StatusCode = 1010 // StatusInternalError indicates an unexpected server condition. StatusInternalError StatusCode = 1011 // StatusServiceRestart indicates that the service is restarting. StatusServiceRestart StatusCode = 1012 // StatusTryAgainLater indicates a temporary server condition. StatusTryAgainLater StatusCode = 1013 // StatusBadGateway indicates an invalid response from an upstream service. StatusBadGateway StatusCode = 1014 // StatusTLSHandshake represents a TLS handshake failure. // It cannot be sent on the wire. StatusTLSHandshake StatusCode = 1015 )
func CloseStatus ¶
func CloseStatus(err error) StatusCode
CloseStatus returns the first CloseError status found in err's wrapped or joined error tree. It accepts both CloseError values and pointers and returns StatusCode(-1) for nil or non-close errors.