Server-Sent Events with Spring

A popular choice for sending real time data from the server to a web application is WebSocket. WebSocket opens a bidirectional connection between client and server. Both parties can send and receive messages. In scenarios where the application only needs one way communication from the server to the client a simpler alternative exists: Server-Sent Events (SSE). It's a HTML5 standard and utilizes HTTP as transport protocol, the protocol only supports text messages and it's unidirectional, only the server can send messages to the client.

Server-Sent Events are supported in most modern browsers. Only Microsoft's browsers IE and Edge do not have a built in implementation. Fortunately this is not a problem because Server-Sent Events uses common HTTP connections and can therefore implemented with a polyfill. The following polyfill libraries area available and add Server-Sent Event support to IE and Edge.

We use the Yaffle/EventSource library in our productive applications and it works so far very reliable.

With SSE the server cannot immediately start sending messages to the client. It's always the client (browser) that establishes the connection and after that the server is able to send messages. A SSE connection is basically a long streaming HTTP connection.

Client opens the HTTP connection

Server sends zero, one or more messages over this connection.

Connection is closed by the server or due to a network error.

Client opens a new HTTP connection and so on...

The nice thing about Server-Sent Events is that they have a built in reconnection feature, when the client loses the connection he tries to reconnect to the server automatically. WebSocket does not have such a built in functionality.

Despite the fact that only the server can send messages to the client over SSE, you could develop applications like a chat application with this technology. Because a client application can always open a second HTTP connection with the Fetch API or XMLHttpRequest and send data to the server.

The Server-Sent Events API in the browser is simple and consist of only one object: EventSource To open a connection an application needs to instantiate the object and provide the URL of the server endpoint.

const eventSource = new EventSource('http://localhost:8080/stream');

The browser immediately sends a GET request to the URL with the accept header text/event-stream

GET /stream HTTP/1.1
Accept: text/event-stream
...

Because this is a normal GET request, an application could add query parameters to the URL like with any other GET request.

Query parameters cannot be changed during the lifecycle of the EventSource object. Every time the client reconnects he uses the same URL. But an application can always close the connection with the close() method and instantiate a new EventSource object.

The HTTP response to a EventSource GET request must contain the Content-Type header with the value text/event-stream and the response must be encoded with UTF-8.

HTTP/1.1 200
Content-Type: text/event-stream;charset=UTF-8

The protocol that SSE uses is text based, starts with a keyword then a colon (:) and a string value. The data keyword specifies a message for the client. Spaces before and after the message will be ignored. Every line is separated with a carriage return (0d) or a line feed (0a) or both (0d 0a).

data: the server message

The server can split a message over several lines

data:line1
data:line2
data:line3

The browser will concatenate these three lines and emit one event. To separate message from each other the server needs to send a blank line after each message.

You see that the payload does not have to be a simple string. It's perfectly legal to send JSON strings and parse them on the client with JSON.parse.

To process these events in the browser an application needs to register a listener for the message event. The property data of the event object contains the message. The browser filters out the keyword data and the colon and only assigns the string after the colon to event.data.

An application can listen for the open and error event. The open event is emitted as soon as the server sends a 200 response back. The error event is fired whenever a network error occurs. It is also emitted when the server closes the connection.

A server can assign an event name to a message with the event: keyword. The event: line can precede or follow the data: line. In this example the server sends 4 messages. The first message is an add event, the second a remove event, then follows again an add event and the last message is an unnamed event.

Named events are processed differently on the client. They do not trigger the message handlers. Named events emit an event that has the same name as the event itself. For this example we need 3 listeners to process all the messages. You cannot use the on... syntax for registering listeners to these events, they have to be registered with the addEventListener function.

Browsers keep the Server-Sent Events HTTP connection open as long as possible. When the connection is closed by the server or due to a network error, the browser waits by default 3 seconds and then opens a new HTTP connection. The browser tries to send reconnect requests forever until he gets a 200 HTTP response back. With a call to close() an application can stop this.

The 3 seconds wait time between connections can be changed by the server. To change it the server sends a retry: line together with the message. The number after the colon specifies the number of milliseconds the browser has to wait before he tries to reconnect.

event:add
data:100
retry:10000

After the browser receives this message, he changes the wait time between connections to 10 seconds. With retry:0 the browser immediately tries to reconnect after the previous connection was closed.

The primary use case for this id is to keep track what messages the client successfully received. When the SSE connection was closed the browser sends a new GET request and in this request he sends the last received message id as an additional HTTP header Last-Event-ID to the server.

Spring introduced support for Server-Sent Events with version 4.2 (Spring Boot 1.3). In the following example we create a Spring Boot application that sends the current used heap and non-heap memory of the Java virtual machine as Server-Sent Events to the client. The client is a simple html page that displays these values.

Next we create a RestController that handles the EventSource GET request from the client. The get handler needs to return an instance of the class SseEmitter. Each client connection is represented with it's own instance of SseEmitter. Spring does not give you tools to manage these SseEmitter instances. In this application we store the emitters in a simple list(emitters) and add handlers to the emitter's completion and timeout event to remove them from the list.

The method onMemoryInfo is annotated with the @EventListener annotation and listens for the events that are sent from the MemoryObserverJob class. When a new object is emitted the method loops over all registered clients and tries to send the MemoryInfo to the clients. The send call can always fail when the client is no longer connected. Servers do not get informed when the EventSource connection is closed by the client either normally with close() or due to a network error or the user just closed the browser. Because of that we add a try catch around the send method and when the send fails the application removes that client from the emitters list.

To send messages to the client the application calls the emitter's send method. This method expects either an object or a SseEventBuilder. Objects will be converted to a JSON string and sent in a data: line to the client. The SseEventBuilder allows the application to set all the previous mentioned message attributes like retry, id and event name. The static method event() of the SseEmitter class creates a new SseEventBuilder.

Spring provides an easy way to access the Last-Event-ID HTTP header when the application needs it. You have to make the parameter optional with required=false because the initial GET request from the client does not contain this HTTP header.

At the end of this blog post a shameless self plug. Because Spring does not provide support for keeping track of the SseEmitter instances I wrote a library that does that for Spring Boot applications. You can add the library with this dependency to a project.

The library expects that each client sends a unique id. An application can create such an id with a uuid library like https://github.com/kelektiv/node-uuid. For starting the SSE connection, the client calls the endpoint with the createSseEmitter method and sends the id and optionally the names of the events he is interested in.