Server-sent Events

Tiffany Brown
  1. Introduction
  2. Subscribing to a stream: the EventSource object
  3. Sending events from the server
    1. Sending message events
    2. Sending custom events
    3. Managing reconnections with the retry interval
    4. Setting a unique identifier with the id field
  4. Handling events
  5. Handling errors
  6. Browser implementation discrepancies
  7. Browser support and fallback strategies

Imagine that your country’s national basketball team is playing for the World Basketball Championship. You want to keep track of the game, but you can’t watch it because it takes place while you’re at work.

Luckily for you, your national news service has a crackerjack web development team. They’ve built a sports ticker that updates with every foul called or basket scored. You visit a URL, and updates are pushed right to your browser. Of course, you wonder how they did that. The answer? Server-sent events.

Server-sent events are way of pushing data and/or DOM events from the server to a client using a stream. It’s handy for stock tickers, sports scores, flight tracking, e-mail notifications — any situation in which data will be updated periodically.

But wait! I hear you saying, Can’t we already do this with technologies like XMLHttpRequest, or Web Sockets? Well, yes. However doing so requires extending those objects to do what EventSource does natively.

Server-side considerations

Because server-sent events are streams of data, they require long-lived connections. You’ll want to use a server can handle large numbers of simultaneous connections. Event-driven servers are, of course, particularly well-suited to streaming events. These include Node.js, Juggernaut, and Twisted. For Nginx, there’s the nginx-push-stream-module. Server configuration is beyond the scope of this article, however, and it will vary with the server you use.

Let’s look at subscribing to a stream using an EventSource object. Then we’ll look at sending and handling events.

Subscribing to an event stream: the EventSource object

Creating an EventSource object is simple.

var evtsrc = new EventSource('./url_of/event_stream/',{withCredentials:false});

The EventSource constructor function accepts up to two parameters:

  • a URL string, which is required; and
  • an optional dictionary parameter that defines the value of the withCredentials property.

Dictionaries resemble objects in their syntax, but they are actually associative data arrays with defined name-value pairs. In this case, withCredentials is the only possible dictionary member. Its value can be true or false. (To learn more about dictionaries in general, refer to the Web IDL specification.)

Including the dictionary parameter is only necessary for cross-origin requests requiring user credentials (cookies). To date, no browser supports cross-origin EventSource requests. As a result, we won’t include the second parameter in our examples.

When the EventSource connection opens, it will fire an open event. We can define a function to handle that event by setting the onopen attribute.

var evtsrc = new EventSource('./url_of/event_stream/');
evtsrc.onopen = function(openevent){
    // do something when the connection opens
}

Should something go wrong with our connection, an error will be fired. We can define a handler function for these events using the onerror attribute. We’ll discuss some causes of error events in the Handling errors section.

evtsrc.onerror = function(openevent){
    // do something when there's an error
}

Streamed events are message events by default. To handle message events, we can use the onmessage attribute to define a handler function.

evtsrc.onmessage = function(openevent){
    // do something when we receive a message event.
}

We can also use addEventListener() to listen for events. This is the only way to handle custom events, as we’ll see in the Handling events section.

var onerrorhandler = function(openevent){
    // do something
}
evtsrc.addEventListener('error',onerrorhandler,false);

To close a connection use the close() method.

evtsrc.close();

So we’ve created our EventSource object, and defined handlers for the open, message, and error events. But in order for this to work, we need a URL that streams events.

Sending events from the server

A server-sent event is a snippet of text delivered as part of a stream from a URL. In order for browsers to treat our data as a stream we must:

  • serve our content with a Content-type header whose value is text/event-stream;
  • use UTF-8 character encoding.

The syntax for a server-sent event is simple. It consists of one or more colon-separated field name-value pairs, followed by an end-of-line character. Field names can contain one of four possible values.

  • data: The information to be sent.
  • event: The type of event being dispatched.
  • id: An identifier for the event to be used when the client reconnects.
  • retry: How many milliseconds should lapse before the browser attempts to reconnect to the URL.

Of these, only the data field is required.

Sending message events

In this example, we will send an event announcing which teams are playing in our championship game. When the browser receives this text, it will dispatch a message event.

data: Brazil v. United States

The value of the data field becomes the value of the message event’s data property. As mentioned above, server-sent events are message events by default. But as we’ll discuss in a bit, we can also dispatch custom events, by including an event field.

We can also send several pieces of data as a single event. Each chunk of data should be followed by an end-of-line character (either a new line character, carriage return character, or both). Here we’re appending an event that containing the location and attendance of this game.

data: Brazil v. United States

:Comments begin with a colon. Events must be followed a blank line.
data: Air Canada Centre
data: Toronto, Ontario, Canada
data: Attendance: 19,800

For this event, the value of the data property will be: Air Canada CentrenToronto, Ontario, CanadanAttendance: 19,800.

Take note of the blank line between events. In order for the client to receive an event, it must be followed by a blank line. Comments begin with a colon.

Sending custom events

Events are of the type message unless we specify otherwise. To do that, we’ll need to include an event field. In the example that follows, we will add two startingfive events to our stream, and send our data as a JSON-formatted string.

event: startingfive
data: {"team":{"country":"Brazil","players":[{"id":15,"name":"de Sousa","position":"C"},{"id":12,"name":"Dantas","position":"F"},
{"id":7,"name":"Jacintho","position":"F"},{"id":6,"name":"de Oliveira Ferreira","position":"G"},{"id":4,"name":"Moisés Pinto","position":"G"}]}}

event: startingfive
data: {"team":{"country":"USA","players":[{"id":15,"name":"Charles","position":"C"},{"id":11,"name":"Cash","position":"F"},
{"id":5,"name":"Jones","position":"F"},{"id":7,"name":"Montgomery","position":"G"},{"id":4,"name":"Pondexter","position":"G"}]}}

Here we need to listen for the startingfive event instead of a message event. Our data field, however, will still become the value of the event’s data property.

We’ll discuss the data property and MessageEvent interface in the Handling events section.

Managing connections and reconnections

Now while it is true that the server pushes events to the browser, the reality is a little more nuanced. If the server keeps the connection open, EventSource request will be one, extended request. If it closes, the browser will wait a few seconds, then reconnect. A connection might close, for example, if the URL sends an end-of-file token.

Each browser sets its own default reconnect interval. Most reconnect after 3 to 6 seconds. You can control this interval, however, by including a retry field. The retry field indicates the number of milliseconds the client should wait before reconnecting to the URL. Let’s build on our example from above and change our event to include a 5-second (5000 milliseconds) retry interval.

event: startingfive
data: {"team":{"country":"USA","players":[{"id":15,"name":"Charles","position":"C"},{"id":11,"name":"Cash","position":"F"},
{"id":5,"name":"Jones","position":"F"},{"id":7,"name":"Montgomery","position":"G"},{"id":4,"name":"Pondexter","position":"G"}]}}
retry: 5000

Event streams can remain active for as long as the client is connected. Depending on your architecture and application, you may want the server to close connections periodically.

Setting a unique identifier with the id field

When the browser reconnects to the URL, it will receive whatever data is available at the point of reconnection. But in the case of a game ticker, we may want to let our visitor catch up on what he or she missed. This is why it’s a best practice to set an id for each event. In the example below, we’re sending an id as part of a score event.

event: score
retry: 3000
data: Brazil 14
data: USA 13
data: 2pt, de Sousa
id: 09:42

Its value should be unique to the stream. In this case, we’re using the time the basket was scored.

The id field becomes the lastEventId property of this event object. But it serves another purpose. Should the connection close, the browser will include a Last-Event-ID header with its next request. Think of it as a bookmark for the stream. If the Last-Event-ID header is present, you can adjust your application’s response to send only those events that succeeded it.

Handling events

As mentioned above, all events are message events by default. Every message event has three attributes, defined by the MessageEvent interface.

event.data
Returns the data or message sent as part of the message event.
event.origin
Returns the origin of the message, which is typically a string containing the scheme (ex: http, https), host name, and port from which the message was sent.
event.lastEventId
Returns the unique identifier of the last event received.

Any time a message event is fired, our onmessage function will be invoked. This works just fine for applications in which you will only send message events. But its limitations become obvious if you’d like to send score or startingfive events as in our example. Using addEventListener is more flexible. In the code below, we’re handling a startingfive event using addEventListener.

var evtsrc = new EventSource('./url_of/event_stream/');

var startingFiveHandler = function(event){
    var data = JSON.parse(event.data), numplayers, pl;

    console.log( data.team.country );

    numplayers = data.team.players.length;

    for(var i=0; i < numplayers; i++){
        pl = '['+data.team.players[i].id+'] '+data.team.players[i].name+', '+data.team.players[i].position;
        console.log( pl );
    }
}

evtsrc.addEventListener('startingfive',startingFiveHandler,false);

Handling errors

Smart error handling requires a bit more work than just setting the onerror attribute. We also need to know whether the error resulted in a failed connection, or a temporarily interrupted one. After a failed connection, the browser will not attempt to reconnect. If it is a temporary interruption — as can occur if the computer was asleep, or the server closes the connection — the browser will try again. Browsers will dispatch an error event for any of the following reasons.

  • The URL sends a Content-type response header with the wrong value.
  • The URL returned an HTTP error header such as 404 File Not Found or 500 Internal Server Error.
  • A network or DNS issue prevented a connection.
  • The server closed the connection.
  • The requesting origin is not one allowed by the URL.

That last point deserves some clarification. To date, no browser supports server-sent event requests across origins. In Firefox and Opera, attempting a cross-origin request will trigger an error event on the EventSource object, and the connection will fail. In Chrome and Safari, it will trigger a DOM security exception instead.

When handling errors, then, it’s important to check the readyState property. Let’s look at an example.

var onerror = function(event){
    var txt;
    switch( event.target.readyState ){
        // if reconnecting
        case EventSource.CONNECTING:
            txt = 'Reconnecting...';
            break;
        // if error was fatal
        case EventSource.CLOSED:
            txt = 'Connection failed. Will not retry.';
            break;
    }
    alert(txt);
}

In the code above, if the value of e.target.readyState is EventSource.CONNECTING (a constant defined by the specification; its value is 0), we will alert the user that we are reconnecting. If its value equals EventSource.CLOSED (another constant whose value is 2), we will alert the user that the browser will not reconnect.

Browser implementation discrepancies

Neither Firefox nor Opera change the EventSource object’s readyState property when the computer wakes from sleep mode. Even though the connection is temporarily lost, the value of EventSource.readyState remains 1. Chrome and Safari, by contrast, change the readyState value to 0, indicating that the browser is reestablishing the connection. In tests, however, all browsers appear to automatically reconnect to the URL several seconds after awaking.

Browser support and fallback strategies

As of publication, Opera 11.60+, Firefox 6.0+, Safari 5.0+, iOS Safari 4.0+, and Chrome 6.0+ all support server-sent events. Android’s WebKit and Opera Mini do not. Since EventSource is a property of the global object (in browsers, this is typically the window object), we can determine support using the following code.

if(window.EventSource !== undefined){
    // create an event source object.
} else {
    // Use a fallback or throw an error.
}

XMLHttpRequest can be used as a fallback for browsers that do not support EventSource. Polyfills that use an XHR fallback include EventSource by Yaffle and EventSource.js by Remy Sharp.

Keep in mind that when using XHR, your URL should ideally close the connection after each request. Doing so ensures maximum browser compatibility.

Of course, your application doesn’t exactly know whether the requesting object was EventSource or XMLHttpRequest, and therefore doesn’t know whether it should close the connection. To solve this issue, include a custom request header when using XMLHttpRequest as shown below.

var xhr = new XMLHttpRequest();
xhr.open('GET','./url_of/event_stream/');
xhr.setRequestHeader('X-Requestor','XHR');
xhr.send(null);

Then ensure that your application closes the connection when this custom header is present. Do this by setting the value of the Content-type: header to text/plain, and (optionally) including a Connection: close header in the URL’s response.

Interlinked Nodes image via Shutterstock

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

No Reader comments

Comments on this post are closed.