Is an EventSource (SSE) supposed to try to reconnect indefinitely?

29,488

Solution 1

Server Side Events work differently in all of the browsers, but they all close the connection during certain circumstances. Chrome, for example, closes the connection on 502 errors while a server is restarted. So, it is best to use a keep-alive as others suggest or reconnect on every error. Keep-alive only reconnects at a specified interval that must be kept long enough to avoid overwhelming the server. Reconnecting on every error has the lowest possible delay. However, it is only possible if you take an approach that keeps server load to a minimum. Below, I demonstrate an approach that reconnects at a reasonable rate.

This code uses a debounce function along with reconnect interval doubling. It works well, connecting at 1 second, 4, 8, 16...up to a maximum of 64 seconds at which it keeps retrying at the same rate. I hope this helps some people.

function isFunction(functionToCheck) {
  return functionToCheck && {}.toString.call(functionToCheck) === '[object Function]';
}

function debounce(func, wait) {
    var timeout;
    var waitFunc;
    
    return function() {
        if (isFunction(wait)) {
            waitFunc = wait;
        }
        else {
            waitFunc = function() { return wait };
        }
        
        var context = this, args = arguments;
        var later = function() {
            timeout = null;
            func.apply(context, args);
        };
        clearTimeout(timeout);
        timeout = setTimeout(later, waitFunc());
    };
}

// reconnectFrequencySeconds doubles every retry
var reconnectFrequencySeconds = 1;
var evtSource;

var reconnectFunc = debounce(function() {
    setupEventSource();
    // Double every attempt to avoid overwhelming server
    reconnectFrequencySeconds *= 2;
    // Max out at ~1 minute as a compromise between user experience and server load
    if (reconnectFrequencySeconds >= 64) {
        reconnectFrequencySeconds = 64;
    }
}, function() { return reconnectFrequencySeconds * 1000 });

function setupEventSource() {
    evtSource = new EventSource(/* URL here */); 
    evtSource.onmessage = function(e) {
      // Handle even here
    };
    evtSource.onopen = function(e) {
      // Reset reconnect frequency upon successful connection
      reconnectFrequencySeconds = 1;
    };
    evtSource.onerror = function(e) {
      evtSource.close();
      reconnectFunc();
    };
}
setupEventSource();

Solution 2

I rewrote the solution of @Wade and after a little bit of testing I came to the conclusion that the functionality stayed the same with less code and better readability (imo).

One thing I did not understand was, why you clear the Timeout if the timeout variable gets set back to null every time you try to reconnect. So I just omitted it completely. And I also omitted the check if the wait argument is a function. I just assume it is, so it makes the code cleaner.

var reconnectFrequencySeconds = 1;
var evtSource;

// Putting these functions in extra variables is just for the sake of readability
var waitFunc = function() { return reconnectFrequencySeconds * 1000 };
var tryToSetupFunc = function() {
    setupEventSource();
    reconnectFrequencySeconds *= 2;
    if (reconnectFrequencySeconds >= 64) {
        reconnectFrequencySeconds = 64;
    }
};

var reconnectFunc = function() { setTimeout(tryToSetupFunc, waitFunc()) };

function setupEventSource() {
    evtSource = new EventSource("url"); 
    evtSource.onmessage = function(e) {
      console.log(e);
    };
    evtSource.onopen = function(e) {
      reconnectFrequencySeconds = 1;
    };
    evtSource.onerror = function(e) {
      evtSource.close();
      reconnectFunc();
    };
}

setupEventSource();

Solution 3

I read the standard the same way as you but, even if not, there are browser bugs to consider, network errors, servers that die but keep the socket open, etc. Therefore, I usually add a keep-alive on top of the re-connect that SSE provides.

On the client-side I do it with a couple of globals and a helper function:

var keepaliveSecs = 20;
var keepaliveTimer = null;

function gotActivity() {
  if (keepaliveTimer != null) {
    clearTimeout(keepaliveTimer);
  }
  keepaliveTimer = setTimeout(connect,keepaliveSecs * 1000);
}

Then I call gotActivity() at the top of connect(), and then every time I get a message. (connect() basically just does the call to new EventSource())

On the server-side, it can either spit out a timestamp (or something) every 15 seconds, on top of normal data flow, or use a timer itself and spit out a timestamp (or something) if the normal data flow goes quiet for 15 seconds.

Solution 4

What I've noticed (in Chrome at least) is that when you close your SSE connection using close() function, it won't try to reconnect again.

var sse = new EventSource("...");
sse.onerror = function() {
    sse.close();
};

Solution 5

In my current Node.js app dev I noticed Chrome automatically reconnects when my app is restarted, but Firefox does not.

ReconnectingEventSource, an EventSource wrapper, is the easiest solution I found.

Works with or without polyfill of your choice.

Share:
29,488
rhyek
Author by

rhyek

Updated on September 11, 2021

Comments

  • rhyek
    rhyek almost 3 years

    I'm working on a project utilizing Server-Sent-Events and have just run into something interesting: connection loss is handled differently between Chrome and Firefox.

    On Chrome 35 or Opera 22, if you lose your connection to the server, it will try to reconnect indefinitely every few seconds until it succeeds. On Firefox 30, on the other hand, it will only try once and then you have to either refresh the page or handle the error event raised and manually reconnect.

    I much prefer the way Chrome or Opera does it, but reading http://www.w3.org/TR/2012/WD-eventsource-20120426/#processing-model, it seems as though once the EventSource tries to reconnect and fails due to a network error or other, it shouldn't retry the connection. Not sure if I'm understanding the spec correctly, though.

    I was set on requiring Firefox to users, mostly based on the fact that you can't have multiple tabs with an event stream from the same URL open on Chrome, but this new finding would probably be more of an issue. Although, if Firefox behaves according to spec then I might as well work around it somehow.

    Edit:

    I'm going to keep targeting Firefox for now. This is how I'm handling reconnections:

    var es = null;
    function initES() {
        if (es == null || es.readyState == 2) { // this is probably not necessary.
            es = new EventSource('/push');
            es.onerror = function(e) {
                if (es.readyState == 2) {
                    setTimeout(initES, 5000);
                }
            };
            //all event listeners should go here.
        }
    }
    initES();
    
  • Lucio Paiva
    Lucio Paiva almost 5 years
    I was about to say you were creating multiple EventSource connections in Chrome and Safari, but then I checked your code again and you're making sure to close the current connection before creating a new one. Good!
  • Dr Fred
    Dr Fred almost 3 years
    If you close the connection, wouldn't the last_event_id be lost for the next connection ?
  • Wade
    Wade almost 3 years
    @DrFred, maybe the last event ID is lost. I have not tried that. The whole point of this code is to work around the failure of browsers to reconnect when the connection is lost. The last event ID is supposed to help with the transparent resending of messages, but the whole mechanism does not work well and works differently in each browser. So, I do not use the last event ID. It is unnecessary.
  • Wade
    Wade almost 3 years
    Hi Tom, the code you have is not the same as the debounce function I used. A debounce function is a rate limiter and does not just double the timeout. Your code will keep doubling the timeout on every error which will quickly reach its max of 64. Lookup "debounce function" on Google.
  • Tom Böttger
    Tom Böttger almost 3 years
    Hi Wade, thanks for your reply. In fact, what I have written is not a debounce function, it is just a way to throttle the tries to reconnect (through incremental delaying). I believe there is no need for a debounce function because it's a closed loop, i.e. no external function is calling the reconnectFunc repeatedly, it gets called only after a connection error. And yes, since my implementation is not a debounce function per definition, it increments the frequency/timeout on every connection loss, but it gets set back to 1 second after the successful reconnect, just as we want it.