3

In this example a client connects to a Node Express server by creating a new EventSource. The server sends an SSE event upon initial connection, and then on a 30 second interval after that. The problem is that the client's onmessage handler is not triggered by the initial SSE event sent upon connection, but it is triggered by all subsequent events. If I stop the server immediately after a connection is made, this actually will trigger an event in the client which shows that the data was in fact received without being handled a moment earlier (or the initial res.write is only triggered by the server being shutdown):

Server:

let data = {mydata: 123};

function eventsHandler(req, res, next) {

  const headers = {
    "Content-Type": "text/event-stream",
    Connection: "keep-alive",
    "Cache-Control": "no-cache",
  };

  res.writeHead(200, headers);
  // After client opens connection we send data string, but client doesn't handle this correctly
  // Event is only triggered on client if I stop the server after sending this message
  res.write(`data: ${JSON.stringify(data)}\n\n`);
  console.log("sent SSE");

  const clientId = Date.now();
  console.log(clientId, " connected");
  const newClient = {
    id: clientId,
    res,
  };
  clients.push(newClient);

  req.on("close", () => {
    console.log(`${clientId} Connection closed`);
    clients = clients.filter((c) => c.id !== clientId);
  });
}

// these events are received and handled correctly on client
function sendEventsToAll(datadict) {
  clients.forEach((c) => c.res.write(`data: ${JSON.stringify(datadict)}\n\n`));
}

// Route to open SSE connection
app.get("/events", eventsHandler);

// Loop to send SSE every 30 seconds - these are all received and handled correctly
async function wait() {
  sendEventsToAll(data);
  setTimeout(wait, 30000);
}

wait();

Browser:

      const events = new EventSource(url)

      // This handles every SSE event except the one that is written immediately upon connection
      // If I stop the server after connection, this will trigger and reveal the data was received but not handled
      events.onmessage = event => {
        const newData = JSON.parse(event.data)
        console.log("newData", newData)
      }

Edit: I should also add I'm using nginx for a reverse proxy - could this be buffering the first res.write instead of sending it immediately for some reason?

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
Feyman81
  • 410
  • 1
  • 4
  • 14
  • Did you take a look at the TCP stream with e.g. Wireshark? And how do you actually distinguish the first from the second event, it seems you're sending exactly the same data? – Bergi Jun 30 '20 at 15:43
  • The console.log("newData") line is only logged in browser on subsequent events (not after the first res.write in the server's eventsHandler function). Nothing is received until the second write event. Putting multiple res.write instructions in the eventsHandler does nothing, but if I perform res.write outside of this function it always seems to work as expected. Haven't tried Wireshark, will have a look. – Feyman81 Jun 30 '20 at 16:14
  • 1
    It might as well be some buffer flushing problem, so that the *previous* event appears in the browser when you write the next one. That's why I was asking to enumerate (or otherwise distinguish) them instead of sending the exactly same data each time. – Bergi Jun 30 '20 at 16:16
  • 1
    Indeed it's a buffer flushing problem. The previous event appears in the browser as soon as I write the next one, or when I shut down the server which immediately triggers the most recent event. Could Nginx be causing this? Can I force the buffer to flush in express without ending the connection? – Feyman81 Jun 30 '20 at 16:35
  • 1
    Just try without nginx to see whether it's the culprit :-) I'd be rather surprised though, this would be a bug imo. – Bergi Jun 30 '20 at 16:37
  • [This](https://stackoverflow.com/q/60217161/1048572) appears to have a workaround. [This](https://stackoverflow.com/q/11335510/1048572) is pretty old but might be relevant. – Bergi Jun 30 '20 at 16:46
  • 1
    Thanks for setting me on the right path. This was caused by proxy_buffering in nginx which is "on" by default. Adding proxy_buffering off; to the nginx config appears to solve the problem. – Feyman81 Jun 30 '20 at 17:52
  • 1
    I wonder if nginx could detect the `Content-Type: text/event-stream` and automatically disable it… I guess we can close the question now. – Bergi Jun 30 '20 at 17:56

1 Answers1

2

This was caused by proxy_buffering in nginx which is "on" by default.

Turning this off in the nginx config resolved the issue:

server {
# server config here...

   location / {
   # proxy config here...
   proxy_buffering off; # resolves issue
   }

}
Feyman81
  • 410
  • 1
  • 4
  • 14