2

I'm trying to implement Server Sent Events in Go. I'm using the Fiber framework, and found an example implementation that worked fine.

But it does not uses channels, so I cannot send events/messages whenever I want, it just keeps sending them at a specified interval.

Then I found another example which does use channels. But it does not uses Fiber, so I tried to mix the two examples. Right now, my code looks like this:

// ... imports

var sseChan chan string
var wg sync.WaitGroup

// main...

func sseHandler(c *fiber.Ctx) error {
    c.Set("Content-Type", "text/event-stream")
    c.Set("Cache-Control", "no-cache")
    c.Set("Connection", "keep-alive")
    c.Set("Transfer-Encoding", "chunked")

    sseChan = make(chan string)

    // wg.Add(1)
    defer func() {
        // wg.Done()
        close(sseChan)
        sseChan = nil
        fmt.Println("defer func called")
    }()

    fmt.Println("client connected")

    c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) {
        for {
            fmt.Println("inside streamwrite loop")
            select {
            case message := <-sseChan:
                fmt.Println("got a msg on sseChan")
                fmt.Fprintf(w, "data: %s\n\n", message)

                err := w.Flush()
                if err != nil {
                    // Refreshing page in web browser will establish a new
                    // SSE connection, but only (the last) one is alive, so
                    // dead connections must be closed here.
                    fmt.Printf("Error while flushing: %v. Closing http connection.\n", err)

                    break
                }
            case <-c.Context().Done():
                fmt.Printf("Client closed connection")
                return
            }
        }
    }))

    // wg.Wait()

    fmt.Println("returnin")
    return nil
}

func fireEvent(c *fiber.Ctx) error {
    if sseChan != nil {
        fmt.Println("sendin msg")
        msg := time.Now().Format("15:04:05")
        sseChan <- msg
    }

    return c.SendString("event fired")
}

When I try to establish a connection from browser (using EventSource API), the backend throws this error:

client connected
returnin
defer func called
inside streamwrite loop
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x560 pc=0x6f157a]

goroutine 4 [running]:
github.com/valyala/fasthttp.(*RequestCtx).Done(...)
        /home/user/go/pkg/mod/github.com/valyala/fasthttp@v1.45.0/server.go:2739
main.sseHandler.func2(0xc00013ebe0?)
        /home/user/Projects/scaling-sse/api-go/main.go:71 +0x9a
github.com/valyala/fasthttp.NewStreamReader.func1()

I can see that the defer functions gets called before the loop in StreamWriter. That results in the sseChan being nil. So I added the wg statements (commented in sseHandler), which prevents the error but I don't get any output in my browser. Also, on closing the tab, the defer function doesn't gets called.

Mustaghees
  • 506
  • 6
  • 16

1 Answers1

0

The panic seems to be generated by this line:

case <-c.Context().Done():

According to https://github.com/gofiber/fiber/issues/429#issuecomment-1500921469 and to what I'm seeing in my code as well, when the client closes the connection the Done() channel doesn't get any value.
On a client connection close the SetBodyStreamWriter will return the next time w.Flush() will generate an error (trying to send something and the connection was closed).

So removing that case will solve the panic (I'm not too sure why is panicking, just assigning the channel c.Context().Done() to another variable doesn't seem to trigger the panic).

In the code you also want to avoid instantiating the sseChan channel in the sseHandler. Also closing it immediately won't make possible to write to it, sseHandler returns immediately as you can see from the logs.

I presume you want to broadcast the events to all the connected clients, in that case you might want to instantiate the sseChan = make(chan string) globally, or in a way that is accessible where is written or read. You would probably want to store a list of sseChan and add one for each new connection (and remove when connection is closed).

I'm working on a repo with an example of using SSE with Fiber https://github.com/emanuelef/sse-go-fiber.

Emanuele Fumagalli
  • 1,476
  • 2
  • 19
  • 31