Reputation: 69
I have a SSE api which updates the frontend when a data has been updated through another POST API endpoint. It seems to be working the first time but when i reload the page, and run a dummy data through the POST API endpoint, i would have to send the data many times before my frontend reads the send data from the SSE. If i do it too many times, i will get an error saying the connection is closed. After that, if i keep sending data through the POST API, it works normally until i reload the page (or assuming close the tab or browser?).
I am assuming that this is due to me not closing the connection whenever the page is reloaded either on the client side or server side. There isn't much documentation or resource about this so i had to look through multiple codes and try to make something out of it.
How would i solve this problem or more precise, how would i close the connection if the page is refreshed, redirected or closed( as long as the person is not on a specific page, i would like to close the connection).
Frontend code to receive the eventstream
const [eventSource, setEventSource] = createSignal(undefined);
createEffect(() => {
setEventSource(new EventSource(url))
console.log("connected");
eventSource().onmessage = (event) => {
// set the data
};
eventSource().onerror = (event) => {
console.log(`Connection Error: ${event}`);
eventSource()?.close();
};
eventSource().onopen = (event) => {
console.log(`Connection Established: ${event}`);
};
});
onCleanup(() => {
console.log("Closing Connection")
eventSource()?.close();
});
Backend code to listen to incoming data from POST request and send it to frontend
func sseFunction(appCtx *fiber.Ctx, dataChannel chan map[string]any) error {
setHeader(appCtx)
ctx := appCtx.Context()
ctx.SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) {
log.Println("SSE Opened")
for {
select {
case message := <-dataChannel:
jsonString, err := json.Marshal(message)
if err != nil {
log.Println(err)
}
fmt.Fprintf(w, "data: %s\n\n", jsonString)
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)
return
}
case <-ctx.Done():
fmt.Printf("Client closed connection")
return
}
}
}))
return nil
}
*** Update 1: After some debugging, it looks like the server is not running when i close the connection but im not sure if its because the client is not sending the close event to the backend or somehow the backend is not receiving it
case <-ctx.Done():
fmt.Printf("Client closed connection")
return
}
Upvotes: 1
Views: 501
Reputation: 69
After doing some extensive research and having my own hypothesis, i eventually had an assumption that fasthttp
context().done()
is not detecting the client's disconnect from the server.
Now the problem is not from fiber
but actually from fasthttp context
that fiber is using. Since it does not run the context().done()
in the select statement, there is no return statement running and it loops back thinking that the connection is still alive. When it tries to send the data to a non existent connection, after multiple send retries, the w.flush()
returns an error which eventually runs the return statement in the error handler block.
Many people did not recommend using gofiber
only because of the fasthttp
(Not sure why but i guess this is one of the problem it creates). So instead, i just rewrote my app using gin and it works perfectly. No Hitches and when the client disconnects, the done channel is called. I did not even need to close
my connection in a onCleanup
in my Solidjs
app.
SSE using Gin
func sseFunction(appCtx *gin.Context, dataChannel chan map[string]any) {
log.Println("Setting Up SSE connection")
setHeader(appCtx)
appCtx.Stream(func(writer io.Writer) bool {
for {
select {
case message, ok := <-dataChannel:
if !ok {
return false
}
log.Println("Sending data from datachannel to SSE")
appCtx.SSEvent("message", message)
message = nil
return true
case <-appCtx.Request.Context().Done():
return false
}
}
})
}
Upvotes: 1