From af93e3382f23d67e40e9cb07aaf547a9974156af Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Thu, 20 Aug 2026 02:40:51 +0100 Subject: [PATCH] fix(sse): gate event dispatch on data presence, not event: alone --- sse.go | 17 +++++++++++------ sse_test.go | 22 ++++++++++++++++++++++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/sse.go b/sse.go index b612488..eb315a4 100644 --- a/sse.go +++ b/sse.go @@ -51,15 +51,21 @@ func readSSE(r io.Reader, fn func(sseEvent) error) error { var ( event string dataLines []string - pending bool + hasData bool ) + // dispatch fires fn on a blank line, but only when at least one + // "data:" line was accumulated — matching the EventSource spec, an + // "event:" field alone (no data) discards silently rather than + // producing a data-less event. It always resets the accumulated + // fields, dispatched or not. dispatch := func() error { - if !pending { + fire := hasData + ev := sseEvent{event: event, data: []byte(strings.Join(dataLines, "\n"))} + event, dataLines, hasData = "", nil, false + if !fire { return nil } - ev := sseEvent{event: event, data: []byte(strings.Join(dataLines, "\n"))} - event, dataLines, pending = "", nil, false return fn(ev) } @@ -80,10 +86,9 @@ func readSSE(r io.Reader, fn func(sseEvent) error) error { switch field { case "event": event = value - pending = true case "data": dataLines = append(dataLines, value) - pending = true + hasData = true case "id", "retry": // recognized fields, intentionally ignored } diff --git a/sse_test.go b/sse_test.go index 10b3046..113ff02 100644 --- a/sse_test.go +++ b/sse_test.go @@ -177,6 +177,28 @@ func TestReadSSE_BlankLinesWithoutFieldsDoNotDispatch(t *testing.T) { } } +// TestReadSSE_EventFieldAloneDoesNotDispatch proves an "event:" field with +// no "data:" line does not produce a data-less event on the blank line +// that follows it — matching the EventSource spec, which discards an +// event whose data buffer is empty even when the event type was set. +func TestReadSSE_EventFieldAloneDoesNotDispatch(t *testing.T) { + raw := "event: ping\n\ndata: real\n\n" + + var got []sseEvent + err := readSSE(strings.NewReader(raw), func(ev sseEvent) error { + got = append(got, ev) + return nil + }) + if err != nil { + t.Fatalf("readSSE: %v", err) + } + + want := []sseEvent{{event: "", data: []byte("real")}} + if !reflect.DeepEqual(got, want) { + t.Errorf("got %#v, want %#v", got, want) + } +} + // TestNewSSEWriter_SetsHeaders proves newSSEWriter sets the three SSE // response headers on construction, before any body write. func TestNewSSEWriter_SetsHeaders(t *testing.T) {