Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit e508057

Browse files
authored
fix: Avoid panic in ServerSentEventSender by keeping handler alive (#4821)
The goroutine launched by `ServerSentEventSender` can perform a write and flush after the calling http handler has exited, at this point the resources (e.g. `http.ResponseWriter`) are no longer safe to use. To work around this issue, heartbeats and sending events are now handled by the goroutine which signals its closure via a channel. This allows the calling handler to ensure it is kept alive until it's safe to exit. Fixes #4807
1 parent a7e5588 commit e508057

File tree

2 files changed

+60
-27
lines changed

2 files changed

+60
-27
lines changed

coderd/httpapi/httpapi.go

+53-26
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,13 @@ import (
66
"encoding/json"
77
"errors"
88
"fmt"
9-
"io"
109
"net/http"
1110
"reflect"
1211
"strings"
13-
"sync"
1412
"time"
1513

1614
"github.com/go-playground/validator/v10"
15+
"golang.org/x/xerrors"
1716

1817
"github.com/coder/coder/coderd/tracing"
1918
"github.com/coder/coder/codersdk"
@@ -174,8 +173,7 @@ func WebsocketCloseSprintf(format string, vars ...any) string {
174173
return msg
175174
}
176175

177-
func ServerSentEventSender(rw http.ResponseWriter, r *http.Request) (func(ctx context.Context, sse codersdk.ServerSentEvent) error, error) {
178-
var mu sync.Mutex
176+
func ServerSentEventSender(rw http.ResponseWriter, r *http.Request) (sendEvent func(ctx context.Context, sse codersdk.ServerSentEvent) error, closed chan struct{}, err error) {
179177
h := rw.Header()
180178
h.Set("Content-Type", "text/event-stream")
181179
h.Set("Cache-Control", "no-cache")
@@ -187,37 +185,50 @@ func ServerSentEventSender(rw http.ResponseWriter, r *http.Request) (func(ctx co
187185
panic("http.ResponseWriter is not http.Flusher")
188186
}
189187

190-
// Send a heartbeat every 15 seconds to avoid the connection being killed.
188+
closed = make(chan struct{})
189+
type sseEvent struct {
190+
payload []byte
191+
errC chan error
192+
}
193+
eventC := make(chan sseEvent)
194+
195+
// Synchronized handling of events (no guarantee of order).
191196
go func() {
197+
defer close(closed)
198+
199+
// Send a heartbeat every 15 seconds to avoid the connection being killed.
192200
ticker := time.NewTicker(time.Second * 15)
193201
defer ticker.Stop()
194202

195203
for {
204+
var event sseEvent
205+
196206
select {
197207
case <-r.Context().Done():
198208
return
209+
case event = <-eventC:
199210
case <-ticker.C:
200-
mu.Lock()
201-
_, err := io.WriteString(rw, fmt.Sprintf("event: %s\n\n", codersdk.ServerSentEventTypePing))
202-
if err != nil {
203-
mu.Unlock()
204-
return
211+
event = sseEvent{
212+
payload: []byte(fmt.Sprintf("event: %s\n\n", codersdk.ServerSentEventTypePing)),
205213
}
206-
f.Flush()
207-
mu.Unlock()
208214
}
209-
}
210-
}()
211215

212-
sendEvent := func(ctx context.Context, sse codersdk.ServerSentEvent) error {
213-
if ctx.Err() != nil {
214-
return ctx.Err()
216+
_, err := rw.Write(event.payload)
217+
if event.errC != nil {
218+
event.errC <- err
219+
}
220+
if err != nil {
221+
return
222+
}
223+
f.Flush()
215224
}
225+
}()
216226

227+
sendEvent = func(ctx context.Context, sse codersdk.ServerSentEvent) error {
217228
buf := &bytes.Buffer{}
218229
enc := json.NewEncoder(buf)
219230

220-
_, err := buf.Write([]byte(fmt.Sprintf("event: %s\ndata: ", sse.Type)))
231+
_, err := buf.WriteString(fmt.Sprintf("event: %s\ndata: ", sse.Type))
221232
if err != nil {
222233
return err
223234
}
@@ -232,16 +243,32 @@ func ServerSentEventSender(rw http.ResponseWriter, r *http.Request) (func(ctx co
232243
return err
233244
}
234245

235-
mu.Lock()
236-
defer mu.Unlock()
237-
_, err = rw.Write(buf.Bytes())
238-
if err != nil {
239-
return err
246+
event := sseEvent{
247+
payload: buf.Bytes(),
248+
errC: make(chan error, 1), // Buffered to prevent deadlock.
240249
}
241-
f.Flush()
242250

243-
return nil
251+
select {
252+
case <-r.Context().Done():
253+
return r.Context().Err()
254+
case <-ctx.Done():
255+
return ctx.Err()
256+
case <-closed:
257+
return xerrors.New("server sent event sender closed")
258+
case eventC <- event:
259+
// Re-check closure signals after sending the event to allow
260+
// for early exit. We don't check closed here because it
261+
// can't happen while processing the event.
262+
select {
263+
case <-r.Context().Done():
264+
return r.Context().Err()
265+
case <-ctx.Done():
266+
return ctx.Err()
267+
case err := <-event.errC:
268+
return err
269+
}
270+
}
244271
}
245272

246-
return sendEvent, nil
273+
return sendEvent, closed, nil
247274
}

coderd/workspaces.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -867,14 +867,18 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) {
867867
return
868868
}
869869

870-
sendEvent, err := httpapi.ServerSentEventSender(rw, r)
870+
sendEvent, senderClosed, err := httpapi.ServerSentEventSender(rw, r)
871871
if err != nil {
872872
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
873873
Message: "Internal error setting up server-sent events.",
874874
Detail: err.Error(),
875875
})
876876
return
877877
}
878+
// Prevent handler from returning until the sender is closed.
879+
defer func() {
880+
<-senderClosed
881+
}()
878882

879883
// Ignore all trace spans after this, they're not too useful.
880884
ctx = trace.ContextWithSpan(ctx, tracing.NoopSpan)
@@ -885,6 +889,8 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) {
885889
select {
886890
case <-ctx.Done():
887891
return
892+
case <-senderClosed:
893+
return
888894
case <-t.C:
889895
workspace, err := api.Database.GetWorkspaceByID(ctx, workspace.ID)
890896
if err != nil {

0 commit comments

Comments
 (0)