Server-sent Events with Django

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:

MechanismDescriptionProsCons
Periodic PollingClient 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 EventsEventSource() 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.
WebSocketBi-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 StreamingHttpResponse class with a generator for str.
  • Follow the protocol prescribed:
    • Format each message with the template {field type}: {message}\n.
    • For ordinary messages, use data as the field type.
    • On the last message, use \n\n to end the event. Otherwise, use \n to end one message and append more message as needed.

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)