$ mkdir sse-demo $ cd sse-demo $ mkdir src
Server-sent events (SSE) make it possible to stream events from a server to a client application. They are appropriate when you only need a unidirectional flow of events from the server to the client (if you need a bidirectional connection, then WebSockets are a better choice).
After reading this guide, you will be able to:
Configure Pedestal to stream SSE connections.
Send events from Pedestal over the connection using {core_async}.
Connect to the server-sent event stream from a JavaScript client.
This guide is for intermediate users who have worked through some sample applications or the first few hello-world.adoc guides. In particular, you should know how to run a build, start a REPL, and start and stop your server.
You do not need to know any other Clojure web frameworks, but if you do, you may find some of the comparisons useful. If not, that’s OK — just skip those sections.
In typical client/server interactions, the client, usually a web browser, sends a single request to the server, and the server responds with a single response.
With SSEs, this is a bit different. The client still sends a single request, and receives a single response, but it doesn’t get the response all at once. The response body "stays open", and the server can keep appending more content to it.
With SSEs, the response content type is text/event-stream. This event stream consists of
multiple "events", short text-based messages.
We’ll be creating a new Pedestal project, adding a simple route to stream a counter using server-sent events,
and testing it with curl.
Create a new project directory with a deps.edn and a source file:
$ mkdir sse-demo $ cd sse-demo $ mkdir src
link:example$server-sent-events/deps.edn[role=include]
Let’s start with a basic home page and the server plumbing, then add SSE support.
We’ll need several namespaces for SSE support, so let’s bring them all in from the start:
link:example$server-sent-events/src/sse_demo.clj[role=include]
A simple handler to verify things are working:
link:example$server-sent-events/src/sse_demo.clj[role=include]
Start the REPL, load the namespace, and verify it works:
$ clj Clojure 1.12 user=> (require 'sse-demo) nil user=> (sse-demo/start) ...
$ curl http://localhost:8890/ Hello, World!
Now let’s add SSE support. We write a stream-ready callback; this function is called once the SSE connection is established. It receives a {core_async} channel and the SSE context as parameters.
You send events on the channel using regular {core_async} functions, such as put! or >!!,
and ultimately terminate the connection by calling clojure.core.async/close! on the channel.
Each event is a map with :name and :data keys (and, optionally, an :id key). All values are converted to strings before being sent.
| The callback must send at least one event in order to properly initialize the stream. If it doesn’t, the client will think the stream is broken and keep reconnecting. |
Importantly, it is not the job of your callback to send all of the events; your callback should set in motion the concurrent machinery that sends the events. Events will continue to be sent to the client even after the callback returns, until either the client closes the connection, or your code closes the channel.
In this example, we send ten counter events at one-second intervals:
link:example$server-sent-events/src/sse_demo.clj[role=include]
| 1 | The work is done in a future, so the callback returns immediately. |
| 2 | Each event is a map with :name and :data keys. |
| 3 | Closing the channel terminates the SSE connection. |
Now we create an interceptor that calls api:start-stream[ns=io.pedestal.http.sse] to initiate the event stream:
link:example$server-sent-events/src/sse_demo.clj[role=include]
| 1 | start-stream sets up the SSE connection and calls stream-ready once it’s established.
It returns the context with a :response key, which is an open-ended streaming response. |
|
An event stream is a particular kind of asynchronous response.
|
Finally, add the /counter route:
link:example$server-sent-events/src/sse_demo.clj[role=include]
And the connector plumbing:
link:example$server-sent-events/src/sse_demo.clj[role=include]
Restart the connector to pick up the changes:
user=> (require :reload 'sse-demo) nil user=> (sse-demo/restart) ...
Now test the event stream with curl in another terminal:
$ curl http://localhost:8890/counter event: counter data: 0 event: counter data: 1 event: counter data: 2 event: counter data: 3 event: counter data: 4 event: counter data: 5 event: counter data: 6 event: counter data: 7 event: counter data: 8 event: counter data: 9
The events arrive at one-second intervals, then the connection closes.
SSE data is always received as a string. If you want to send JSON (or any other data format), it’s up to the sender to encode it and the receiver to decode it accordingly.
| None of the Pedestal interceptors are invoked when sending SSE events. The interceptors are used for the initial connection request from the client, but not on the events themselves. |
To consume the event stream from a browser, use the standard EventSource API:
const eventSource = new EventSource("/counter");
eventSource.addEventListener("counter", (event) => {
console.log("Counter:", event.data);
});
Coverage of the JavaScript EventSource (SSE) API is beyond the scope of this Pedestal guide. Consult the MDN documentation for a thorough introduction.
For reference, here is the complete source:
link:example$server-sent-events/src/sse_demo.clj[role=include]
link:example$server-sent-events/deps.edn[role=include]
We’ve covered a minimal setup for server-sent event configuration and usage and
demonstrated how to receive events with curl and from JavaScript.
For more details, see the Server-Sent Events Reference.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |