Skip to content

chore: add support for one-way websockets to backend #16853

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Mar 28, 2025
Prev Previous commit
Next Next commit
fix: apply feedback
  • Loading branch information
Parkreiner committed Mar 19, 2025
commit 43b16766c19730e74cfd2c07b431f1542e70c3c0
18 changes: 7 additions & 11 deletions coderd/httpapi/httpapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ func WebsocketCloseSprintf(format string, vars ...any) string {
return msg
}

type InitializeConnectionCallback func(rw http.ResponseWriter, r *http.Request) (
type EventSender func(rw http.ResponseWriter, r *http.Request) (
sendEvent func(sse codersdk.ServerSentEvent) error,
done <-chan struct{},
err error,
Expand Down Expand Up @@ -410,8 +410,8 @@ func ServerSentEventSender(rw http.ResponseWriter, r *http.Request) (
return sendEvent, closed, nil
}

// OneWayWebSocket establishes a new WebSocket connection that enforces one-way
// communication from the server to the client.
// WebSocketEventSender establishes a new WebSocket connection that enforces
// one-way communication from the server to the client.
//
// The function returned allows you to send a single message to the client,
// while the channel lets you listen for when the connection closes.
Expand All @@ -422,7 +422,7 @@ func ServerSentEventSender(rw http.ResponseWriter, r *http.Request) (
// open a workspace in multiple tabs, the entire UI can start to lock up.
// WebSockets have no such limitation, no matter what HTTP protocol was used to
// establish the connection.
func OneWayWebSocket(rw http.ResponseWriter, r *http.Request) (
func WebSocketEventSender(rw http.ResponseWriter, r *http.Request) (
func(event codersdk.ServerSentEvent) error,
<-chan struct{},
error,
Expand All @@ -436,12 +436,8 @@ func OneWayWebSocket(rw http.ResponseWriter, r *http.Request) (
}
go Heartbeat(ctx, socket)

type SocketError struct {
Code websocket.StatusCode
Reason string
}
eventC := make(chan codersdk.ServerSentEvent)
socketErrC := make(chan SocketError, 1)
socketErrC := make(chan websocket.CloseError, 1)
closed := make(chan struct{})
go func() {
defer cancel()
Expand Down Expand Up @@ -477,13 +473,13 @@ func OneWayWebSocket(rw http.ResponseWriter, r *http.Request) (
return
}
if err != nil {
socketErrC <- SocketError{
socketErrC <- websocket.CloseError{
Code: websocket.StatusInternalError,
Reason: "Unable to process invalid message from client",
}
return
}
socketErrC <- SocketError{
socketErrC <- websocket.CloseError{
Code: websocket.StatusProtocolError,
Reason: "Clients cannot send messages for one-way WebSockets",
}
Expand Down
16 changes: 8 additions & 8 deletions coderd/httpapi/httpapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ func TestWebsocketCloseMsg(t *testing.T) {
}

// Our WebSocket library accepts any arbitrary ResponseWriter at the type level,
// but it must also implement http.Hijack
// but the writer must also implement http.Hijacker for long-lived connections
type mockWsResponseWriter struct {
serverRecorder *httptest.ResponseRecorder
serverConn net.Conn
Expand Down Expand Up @@ -196,7 +196,7 @@ func (w mockWsWrite) Write(b []byte) (int, error) {
return w(b)
}

func TestOneWayWebSocket(t *testing.T) {
func TestWebSocketEventSender(t *testing.T) {
t.Parallel()

newBaseRequest := func(ctx context.Context) *http.Request {
Expand Down Expand Up @@ -256,7 +256,7 @@ func TestOneWayWebSocket(t *testing.T) {
req.Proto = p.proto

writer := newWebsocketWriter()
_, _, err := httpapi.OneWayWebSocket(writer, req)
_, _, err := httpapi.WebSocketEventSender(writer, req)
require.ErrorContains(t, err, p.proto)
}
})
Expand All @@ -267,7 +267,7 @@ func TestOneWayWebSocket(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitShort)
req := newBaseRequest(ctx)
writer := newWebsocketWriter()
send, _, err := httpapi.OneWayWebSocket(writer, req)
send, _, err := httpapi.WebSocketEventSender(writer, req)
require.NoError(t, err)

serverPayload := codersdk.ServerSentEvent{
Expand All @@ -293,7 +293,7 @@ func TestOneWayWebSocket(t *testing.T) {
ctx, cancel := context.WithCancel(testutil.Context(t, testutil.WaitShort))
req := newBaseRequest(ctx)
writer := newWebsocketWriter()
_, done, err := httpapi.OneWayWebSocket(writer, req)
_, done, err := httpapi.WebSocketEventSender(writer, req)
require.NoError(t, err)

successC := make(chan bool)
Expand All @@ -317,7 +317,7 @@ func TestOneWayWebSocket(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitShort)
req := newBaseRequest(ctx)
writer := newWebsocketWriter()
_, done, err := httpapi.OneWayWebSocket(writer, req)
_, done, err := httpapi.WebSocketEventSender(writer, req)
require.NoError(t, err)

successC := make(chan bool)
Expand Down Expand Up @@ -347,7 +347,7 @@ func TestOneWayWebSocket(t *testing.T) {
ctx, cancel := context.WithCancel(testutil.Context(t, testutil.WaitShort))
req := newBaseRequest(ctx)
writer := newWebsocketWriter()
send, done, err := httpapi.OneWayWebSocket(writer, req)
send, done, err := httpapi.WebSocketEventSender(writer, req)
require.NoError(t, err)

successC := make(chan bool)
Expand Down Expand Up @@ -388,7 +388,7 @@ func TestOneWayWebSocket(t *testing.T) {
ctx := testutil.Context(t, timeout)
req := newBaseRequest(ctx)
writer := newWebsocketWriter()
_, _, err := httpapi.OneWayWebSocket(writer, req)
_, _, err := httpapi.WebSocketEventSender(writer, req)
require.NoError(t, err)

type Result struct {
Expand Down
4 changes: 2 additions & 2 deletions coderd/workspaceagents.go
Original file line number Diff line number Diff line change
Expand Up @@ -1109,13 +1109,13 @@ func (api *API) watchWorkspaceAgentMetadataSSE(rw http.ResponseWriter, r *http.R
// @Router /workspaceagents/{workspaceagent}/watch-metadata-ws [get]
// @x-apidocgen {"skip": true}
func (api *API) watchWorkspaceAgentMetadataWS(rw http.ResponseWriter, r *http.Request) {
api.watchWorkspaceAgentMetadata(rw, r, httpapi.OneWayWebSocket)
api.watchWorkspaceAgentMetadata(rw, r, httpapi.WebSocketEventSender)
}

func (api *API) watchWorkspaceAgentMetadata(
rw http.ResponseWriter,
r *http.Request,
connect httpapi.InitializeConnectionCallback,
connect httpapi.EventSender,
) {
// Allow us to interrupt watch via cancel.
ctx, cancel := context.WithCancel(r.Context())
Expand Down
4 changes: 2 additions & 2 deletions coderd/workspaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -1732,13 +1732,13 @@ func (api *API) watchWorkspaceSSE(rw http.ResponseWriter, r *http.Request) {
// @Success 200 {object} codersdk.ServerSentEvent
// @Router /workspaces/{workspace}/watch-ws [get]
func (api *API) watchWorkspaceWS(rw http.ResponseWriter, r *http.Request) {
api.watchWorkspace(rw, r, httpapi.OneWayWebSocket)
api.watchWorkspace(rw, r, httpapi.WebSocketEventSender)
}

func (api *API) watchWorkspace(
rw http.ResponseWriter,
r *http.Request,
connect httpapi.InitializeConnectionCallback,
connect httpapi.EventSender,
) {
ctx := r.Context()
workspace := httpmw.WorkspaceParam(r)
Expand Down