Server-sent Events are an easy way to have a long-lasting Web request in place that periodically send updates and/or statuses.
It’s a long-polling mechanism sitting between periodic stateless pings from clients and full-duplex persistent channel between clients and servers:
| Mechanism | Description | Pros | Cons |
|---|---|---|---|
| Periodic Polling | Client calls setinterval() with normal stateless fetch() pattern. | No persistent connections Robust, self-heals | Latency exists between server and client states, depending on polling period. |
| Server-send Events | EventSource() with handlers on messages from server. Dedicated GET endpoint on server. Prescribed data protocol. | Easy to set up client-side. | – Persistent connection. – Dedicated GET endpoint. – Unidirectional messages from server. |
| WebSocket | Bi-directional signals and messages from client to server. | Bi-directional messages | – Persistent connection. – Complexity. |
Client-side
The client code is pleasantly simple:
const es = new EventSource("http://localhost:8000/endpoint/?abc=xyz&...")
// NOTE: onmessage, not onMessage
es.onmessage((event) => {
const data = event.data
// asynchronous processing of data from server
...
})
// Optional: a "poison pill" in the data can also be used in onmessage() handling
// to detect end of session.
es.addEventListener("done", (event) => {
es.close()
})
The cool thing is that disruptions will be automatically handled and connection is reestablished by the browser runtime. The connection WILL end upon a call to the close() method on the EventSource.
One limitation here is that the HTTP method is LOCKED to GET. I cannot use POST with a JSON body for parameters. Whatever parameters I want to pass to the server will need to be URI-encoded and passed via the query parameters of the GET request.
Another limitation (perhaps by design) is that messages flow only one way: from server to client. The Javascript code will not and cannot send any data to the server beyond that initial GET request.
Of course, until the EventSource is closed, we’re holding one persistent connection open on the server, so use with care. Supposedly there is a limit of 6 such connections per URL.
Server-side (Django)
import time
from typing import Generator
from django.http import HttpRequest, HttpResponseBase, StreamingHttpResponse
from django.views import View
class SSESampleView(View):
def _format_message(self, field_data: str, field_type: str = "data") -> str:
return f"{field_type}: {field_data}\n\n"
def _event_generator(self) -> Generator[str, None, None]:
for i in range(10):
yield self._format_message("Message # {i}")
time.sleep(1)
yield self._format_message("", field_type="done")
def get(self, request: HttpRequest) -> HttpResponseBase:
response = StreamingHttpResponse(
self._event_generator(),
content_type='text/event-stream'
)
response["Cache-Control"] = "no-cache"
return response
Things to note:
- Use of the
StreamingHttpResponseclass with a generator forstr. - Follow the protocol prescribed:
- Format each message with the template
{field type}: {message}\n. - For ordinary messages, use
dataas the field type. - On the last message, use
\n\nto end the event. Otherwise, use\nto end one message and append more message as needed.
- Format each message with the template
A simple one-liner event:
data: This is a simple message for the event\n\n
A multi-line event:
data: This is the first message of the event.\ndata:2nd and last message of event.\n\n
A custom event (done) with data:
event: done\ndata: This is the end.\ndata: It really is.\n\n
Read More
Server-sent events – Web APIs | MDN — the spec on SSE (not specific to Django; in fact, they use PHP)