Actually make SseWriter be spec compliant
- Change inbound data type from byte slice to dedicated event message type - Include data validation for id and event type - Better documentation
This commit is contained in:
parent
51b35036ab
commit
3bb1984f1c
1 changed files with 46 additions and 5 deletions
51
http/sse.go
51
http/sse.go
|
@ -4,18 +4,36 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type SseEvent struct {
|
||||||
|
Type *string
|
||||||
|
Id *string
|
||||||
|
Data string
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrWriterNotFlushable = errors.New("response writer not flushable")
|
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
|
// SseWriter provides a simple implementation for sending data via Server Side Events
|
||||||
// to the client. The function runs until the dataStream channel is closed.
|
// to the client. The function returns once the dataStream channel is closed.
|
||||||
// The ResponseWriter must not be used after calling this function
|
// 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
|
// 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("Access-Control-Expose-Headers", "Content-Type")
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/event-stream")
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
|
@ -27,11 +45,34 @@ func SseWriter(w http.ResponseWriter, dataStream chan []byte) error {
|
||||||
return ErrWriterNotFlushable
|
return ErrWriterNotFlushable
|
||||||
}
|
}
|
||||||
for data := range dataStream {
|
for data := range dataStream {
|
||||||
_, err := fmt.Fprintf(w, "%s\n\n", string(data))
|
if err := ValidateSseEvent(&data); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
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()
|
flusher.Flush()
|
||||||
}
|
}
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue