diff --git a/http/sse.go b/http/sse.go index 8f72b99..76b4f42 100644 --- a/http/sse.go +++ b/http/sse.go @@ -4,18 +4,36 @@ import ( "errors" "fmt" "net/http" + "strings" ) +type SseEvent struct { + Type *string + Id *string + Data string +} + var ( ErrWriterNotFlushable = errors.New("response writer not flushable") + ErrEventTypeNoNewline = errors.New("event type contained a newline") + ErrEventIdNoNewline = errors.New("event id contained a newline") ) // SseWriter provides a simple implementation for sending data via Server Side Events -// to the client. The function runs until the dataStream channel is closed. -// The ResponseWriter must not be used after calling this function +// to the client. The function returns once the dataStream channel is closed. +// The ResponseWriter should not be used after calling this function +// +// Event rules: +// - SseEvent.Type must not contain any newlines +// - SseEvent.Id must not contain any newlines +// - "\r\n" in SseEvent.Data will be replaced with "\n", afterwards "\r" will also be replaced with "\n" // // Inspired by and partially copied from https://medium.com/@rian.eka.cahya/server-sent-event-sse-with-go-10592d9c2aa1 -func SseWriter(w http.ResponseWriter, dataStream chan []byte) error { +// Also why this and not github.com/tmaxmax/go-sse? +// go-sse is not flexible in its use case. You have one handler that doesn't differentiate between clients. +// This, of course, does not work when you have to send individualised notifications as everyone would +// get the same messages, causing confusion in the best case and data leaks in the worst case +func SseWriter(w http.ResponseWriter, dataStream chan SseEvent) error { w.Header().Set("Access-Control-Expose-Headers", "Content-Type") w.Header().Set("Content-Type", "text/event-stream") @@ -27,11 +45,34 @@ func SseWriter(w http.ResponseWriter, dataStream chan []byte) error { return ErrWriterNotFlushable } for data := range dataStream { - _, err := fmt.Fprintf(w, "%s\n\n", string(data)) - if err != nil { + if err := ValidateSseEvent(&data); err != nil { return err } + if data.Type != nil { + _, _ = fmt.Fprintf(w, "event: %s\n", *data.Type) + } + if data.Id != nil { + _, _ = fmt.Fprintf(w, "id: %s\n", *data.Id) + } + noCr := strings.ReplaceAll(data.Data, "\r\n", "\n") + noCr = strings.ReplaceAll(noCr, "\r", "\n") + for _, line := range strings.Split(noCr, "\n") { + _, _ = fmt.Fprintf(w, "data: %s\n", line) + } + _, _ = fmt.Fprint(w, "\n\n") flusher.Flush() } return nil } + +// Check if a given event is valid for sending via SseWriter +// This function is also internally called by SseWriter for each outbound event +func ValidateSseEvent(e *SseEvent) error { + if e.Type != nil && strings.ContainsRune(*e.Type, '\n') { + return ErrEventTypeNoNewline + } + if e.Id != nil && strings.ContainsRune(*e.Id, '\n') { + return ErrEventIdNoNewline + } + return nil +}