4

When I try to do SSE on express the server stops responding every time after exactly 5 attempts. I would like to have it working indefinitely as it should.

If I leave the page alone after a while this error appears:

"POST http://localhost:3000/api/update net::ERR_EMPTY_RESPONSE"

and on the server:

"MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 message listeners added to [EventEmitter]. Use emitter.setMaxListeners() to increase limit"

Both errors don't appear immediately.

I tried changing headers and sending different statuses but without any effect except breaking even what is working.

I am fairly new to express and node in general, I don't really know what I am doing wrong here.

My server is set-up like this:

app.get("/api/update", (req, res, next) => {
    res.status(200).set({
        "Content-Type": "text/event-stream; charset=utf-8",
        "Cache-Control": "no-cache, no-transform",
        "Transfer-Encoding": "chunked",
        Connection: "keep-alive"
    });
    app.on("message", data => {
        res.write(`data: ${JSON.stringify(data)}\n\n`);
    });
});

app.post("/api/update", (req, res, next) => {
    const message = req.body.type;
    app.emit("message", {
        title: "Update",
        message,
        timestamp: new Date()
    });
});

my client can be approximated by something like this:

import React, {Component} from "react";

class Button extends Component {
    source = new EventSource("api/update");

    messageHandler = event => {
        console.log(event.data);
    };

    componentDidMount = () => {
        this.source.onmessage = this.messageHandler;
    };

    render = () => (
        <button
            onClick={() => {
                fetch("/api/update", {
                    method: "POST",
                    headers: {
                        Accept: "application/json",
                        "Content-Type": "application/json"
                    },
                    body: JSON.stringify({type: "button"})
                });
            }}
        />
    );
}

export default Button;

2 Answers2

3

This part:

class Button extends Component {
    source = new EventSource("api/update");

is the reason: you can have max 5 or 6 simultanous EventSource connections at a time. (SSE(EventSource): why no more than 6 connections?)

Generally, you open new EventSource with every render without closing the old one. With every render you open new instance. You should not open new connection, before the old one is closed.

I use this approach: 1. Store your EventSource listener in useRef, that is kept over all renders. 2. useCallback on your listen function

const evtSrc = useRef(null)
const listenEvt = useCallback(() => { 
  if (!evtSrc.current) {
   evtSrc.current = new EventSource("api/update");
  }
 }, [])
  1. Then you ask for creating new EventSource connection on mount with useEffect hook and close it on every unmount:
    useEffect(() => {
    listenEvt() // componentDidMount
    return () => evtSrc.current.close() // componentDidUnmount
    }

Hope this helps.

Fide
  • 1,127
  • 8
  • 7
  • Where should I put the onmessage method? Into the useCallback hook right after `evtSrc.current = new EventSource("api/update");`? – Davide Pozzani Dec 10 '19 at 08:23
  • 1
    Yes, in that function you define all EventSource methods. – Fide Dec 10 '19 at 08:39
  • I managed to fix the problem but unfortunately not by using this answer: I noticed that I simply forgot to close the connection when posting to the server. The problem was server-side, not client-side. What you said about the re-render is not true in a class component but only in a functional component. Thanks anyway for taking the time to answer my question. – Davide Pozzani Dec 10 '19 at 09:36
0

I managed to fix the problem: I forgot to close the connection when posting to the server. This:

app.post("/api/update", (req, res, next) => {
    const message = req.body.type;
    app.emit("message", {
        title: "Update",
        message,
        timestamp: new Date()
    });
});

becomes this:

app.post("/api/update", (req, res, next) => {
    const message = req.body.type;
    app.emit("message", {
        title: "Update",
        message,
        timestamp: new Date()
    });
    res.end();
});