Server-sent Events
- Introduction
- Subscribing to a stream: the
EventSource
object - Sending events from the server
- Handling events
- Handling errors
- Browser implementation discrepancies
- 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
Well, yes. However doing so requires extending those objects to do what XMLHttpRequest
, or Web Sockets?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 istext/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
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
or500 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