Outdated documentation
You are reading documentation for v3, which is an outdated version. Click here to go to the latest documentation.
# Websocket Since v3.7.0
# Introduction
Websocket is a protocol defined in RFC 6455 (opens new window) allowing duplex communication channels using a single TCP connection. This is especially useful for real-time communication between the clients and the server. Websockets can be used for chat applications for example.
Websocket connections have the following life-cycle:
- The client requests the server using HTTP on a dedicated route. The client expresses that it wants to upgrade its connection using HTTP headers.
- The server upgrades the connection by switching protocols.
- The connection is kept alive and both peers can communicate in either way.
- Either the client or the server decides to close the connection. The close handshake is performed before the TCP connection is closed.
Goyave is using gorilla/websocket
(opens new window) and adds a layer of abstraction to it to make it easier to use. You don't have to write the connection upgrading logic nor the close handshake. Just like regular HTTP handlers, websocket handlers benefit from reliable error handling and panic recovery.
First, import the websocket package:
import "goyave.dev/goyave/v3/websocket"
You may need the gorilla/weboscket
package too. If so, import it with an alias, such as ws
:
import ws "github.com/gorilla/websocket"
# Connection upgrade
The first step in adding websockets to your application is to register a route aimed at upgrading the connection to a websocket connection.
upgrader := websocket.Upgrader{}
router.Get("/websocket", upgrader.Handler(myWebsocketHandler))
upgrader.Handler()
create an HTTP handler upgrading the HTTP connection before passing it to the given websocket.Handler
. Learn more about websocket handlers below, the websocket handler section. After a successful upgrade, the HTTP response status is set to "101 Switching Protocols".
WARNING
Upgraded connections are hijacked (opens new window). It is advised to read about the implications of hijacking in Goyave here.
# Upgrade options
# UpgradeErrorHandler
UpgradeErrorHandler specifies the function for generating HTTP error responses.
The default UpgradeErrorHandler returns a JSON response containing the status text corresponding to the status code returned. If debugging is enabled, the reason error message is returned instead.
{"error": "message"}
websocket.UpgradeErrorHandler
is an alias for func(response *goyave.Response, request *goyave.Request, status int, reason error)
.
Example:
upgrader := websocket.Upgrader{
UpgradeErrorHandler: func(response *goyave.Response, request *goyave.Request, status int, reason error) {
text := http.StatusText(status)
if config.GetBool("app.debug") && reason != nil {
text = reason.Error()
}
message := map[string]string{
"error": text,
}
response.JSON(status, message)
},
}
# ErrorHandler
ErrorHandler specifies the function handling errors returned by websocket Handler. If nil, the error is written to "goyave.ErrLogger". If the error is caused by a panic and debugging is enabled, the default ErrorHandler also writes the stacktrace. See the error handling section for more details.
# CheckOrigin
CheckOrigin (func(r *goyave.Request) bool
) returns true if the request Origin header is acceptable. If CheckOrigin is nil
, then a safe default is used: return false if the Origin request header is present and the origin host is not equal to request Host
header.
A CheckOrigin function should carefully validate the request origin to prevent cross-site request forgery.
# Headers
Headers is a function (func(request *goyave.Request) http.Header
) generating headers to be sent with the protocol switching response.
Example:
upgrader := websocket.Upgrader{
Headers: func(request *goyave.Request) http.Header {
h := http.Header{}
h.Set("X-Custom-Header", "value")
return h
},
}
# Settings
Settings the parameters of the underlying gorilla/websocket
Upgrader
for upgrading the connection. Check their documentation (opens new window) for more details.
"Error" and "CheckOrigin" are ignored: use the Goyave upgrader's "UpgradeErrorHandler" and "CheckOrigin".
# Websocket handlers
Websocket connections use a different type of handler: websocket.Handler
, which is an alias for func(*websocket.Conn, *goyave.Request) error
. The request
parameter contains the original upgraded HTTP request.
To keep the connection alive, these handlers should run an infinite for loop that can return on error or exit in a predictable manner. They also can start goroutines for reads and writes, but shouldn't return before both of them do. The Handler is responsible of synchronizing the goroutines it started, and ensure no reader nor writer are still active when it returns.
If the websocket handler returns nil
, it means that everything went fine and the connection can be closed normally. On the other hand, the websocket handler can return an error, such as a write error, to indicate that the connection should not be closed normally.
The following websocket Handler
is an example of an "echo" feature using websockets:
func Echo(c *websocket.Conn, request *goyave.Request) error {
for {
mt, message, err := c.ReadMessage()
if err != nil {
return err
}
goyave.Logger.Printf("recv: %s", message)
err = c.WriteMessage(mt, message)
if err != nil {
return fmt.Errorf("write: %w", err)
}
}
}
TIP
- Learn more about the available functions on the
gorilla/websocket
documentation (opens new window). - For a more complex example, check out the chat application example (opens new window).
When using the built-in Upgrader
and its Upgrader.Handler()
function, the connection is closed automatically after the websocket Handler
returns, using the closing handshake defined by RFC 6455 Section 1.4 if possible. If the websocket Handler
returns an error that is not a CloseError
, the Upgrader
's error handler will be executed and the close frame sent to the client will have status code 1011 (internal server error) and "Internal server error" as message. If debug is enabled, the message will be set to the one of the error returned by the websocket Handler
. Otherwise the close frame will have status code 1000 (normal closure) and "Server closed connection" as a message.
# Error handling
The HTTP handler returned by Upgrader.Handler()
handles errors returned by websocket Handler
. If the returned error is not nil, the Upgrader
's ErrorHandler
will be executed. By default, the error is printed to goyave.ErrLogger
, but this behavior can be overridden.
It also features a panic recovery mechanism. If the websocket Handler
panics, the connection will be gracefully closed just like if the websocket Handler
returned an error without panicking. The error passed to the ErrorHandler
in case of error is a *websocket.PanicError
, wrapping the original error, and containing the stacktrace if debugging is enabled.
upgrader := websocket.Upgrader{
ErrorHandler: func(request *goyave.Request, err error) {
goyave.ErrLogger.Println(err)
if e, ok := err.(*websocket.PanicError); ok {
// The websocket Handler panicked
if e.Stacktrace != "" {
goyave.ErrLogger.Println(e.Stacktrace)
}
}
},
}
# Return an error or panic?
The websocket Handler
should only panic in case of unexpected error, such as "invalid memory address or nil pointer dereference" and other programming errors. Return an error for everything else (database access error, read/write errors, failed calls to other backend services and APIs, etc.).
# Testing
To test websockets, you have to open a client connection from your test, write to it, then send a close frame. The following piece of code is a test for the "echo" handler seen in a previous example:
import (
"testing"
"goyave.dev/goyave/v3"
"goyave.dev/goyave/v3/config"
ws "github.com/gorilla/websocket"
"github.com/username/myproject/route"
)
type WebsocketTestSuite struct {
goyave.TestSuite
}
func (suite *WebsocketTestSuite) TestUpgrade() {
suite.RunServer(route.Register, func() {
conn, _, err := ws.DefaultDialer.Dial(goyave.BaseURL()+"/websocket", nil)
if err != nil {
suite.Error(err)
return
}
defer conn.Close()
message := []byte("hello world")
suite.Nil(conn.WriteMessage(ws.TextMessage, message))
messageType, data, err := conn.ReadMessage()
suite.Nil(err)
suite.Equal(ws.TextMessage, messageType)
suite.Equal(message, data)
m := ws.FormatCloseMessage(ws.CloseNormalClosure, "Connection closed by client")
suite.Nil(conn.WriteControl(ws.CloseMessage, m, time.Now().Add(time.Second)))
})
}
func TestWebsocketSuite(t *testing.T) {
goyave.RunTest(t, new(WebsocketTestSuite))
}