Python Patterns - Server-Sent Events

I have a love/hate relationship with live updates on web applications, partly because they’re usually a hassle to get right and partly due to their often needing major re-plumbing to work at all.

If you’re just tuning in, this is the second post that I’ve been meaning to write.

Most people reach for socket.io or WebSockets for various reasons, but the truth is that unless you’re trying to do something like collaborative editing or real-time multi-player gaming, they’re overkill and a royal pain to get working properly with both your target audience and your current web server and back-end stack. Furthermore, 95% of the time these things are used for _uni_directional communication.

Which is why I much prefer Server-Sent Events – it works fabulously for sending continuous updates to most browsers, and with the right polyfill it will fall back to polling even on extreme conditions like the server (or network) going away and returning.

Of course you still need to maintain a live connection for each client, etc., but at least it’s standard HTTP and you don’t have to fiddle with a separate server1.

I’ve had a lot of fun playing around with live updating dashboards for kicks in and , but of late I’ve wanted a simple, straightforward -based solution I could integrate with on my (which, incidentally, I’ve been tweaking on Github).

And this week I’ve been toying with the notion of building a front-end to our room reservation system using devices, so I decided to jot down a few notes on how I managed to provide live updates to those.

Making The Bottle Last

First off, how do we cope with persistent connections? Well, that’s easy enough: I use uWSGI with gevent workers (or gunicorn on older setups), but getting a standalone Bottle server going for development is as easy as:

from bottle import run
from gevent import monkey; monkey.patch_all()

# ... your code here

run(server="gevent")

Pushing Events

Now for the clever bits. First off, we need to pack replies in header: value format, with events separated by an extra newline:

def sse_pack(d):
    """Pack data in SSE format"""
    buffer = ''
    for k in ['retry','id','event','data']:
        if k in d.keys():
            buffer += '%s: %s\n' % (k, d[k])
    return buffer + '\n'

Next we need a way to keep sending events while the connection is open.

Fortunately, Bottle running under gevent will gleefully take a generator and keep piping whatever you yield into the open socket, so the rest is merely a matter of incrementing event IDs and keeping state consistent:

import json
from bottle import request, response, get, route

@get("/stream")
def stream_generator():
    # Keep event IDs consistent
    event_id = 0
    if 'Last-Event-Id' in request.headers:
        event_id = int(request.headers['Last-Event-Id']) + 1

    # Set up our message payload with a retry value in case of connection failure
    # (that's also the polling interval to be used as fallback by our polyfill)
    msg = {
        'retry': '2000'
    }

    # Provide an initial data dump to each new client
    response.headers['content-type'] = 'text/event-stream'
    response.headers['Access-Control-Allow-Origin'] = '*'
    msg.update({
         'event': 'init',
         'data' : json.dumps(get_current_shared_state()),
         'id'   : event_id
    })
    yield sse_pack(msg)

    # Now give them deltas as they arrive (say, from a message broker)
    event_id += 1    
    msg['event'] = 'delta'
    while True:
        # block until you get new data (from a queue, pub/sub, zmq, etc.)
        msg.update({
             'event': 'delta',
             'data' : json.dumps(queue.recv()),
             'id'   : event_id
        })
        yield sse_pack(msg)
        event_id += 1

I’ve done this kind of thing so far with Redis pubsub, MQTT brokers, 0MQ sockets, you name it – you might need to do a little more work if you’re not getting your client updates from a blocking mechanism, but this is the gist of things.

Dealing With CORS

But what if you’re providing this stream to a statically-hosted single-page app?

(As I was, yesterday afternoon, live editing the HTML on MEO Cloud and gathering events from my laptop.)

Well, that’s easy enough: CORS-compliant browsers will issue an OPTIONS request to your server before actually requesting the event stream, so we can tell them it’s OK to have requests come in from pages hosted in other domains and specify which headers they are allowed to use:

@route("/stream", method="OPTIONS")
def options():
    response.headers.update({
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': 'GET, OPTIONS',
        'Access-Control-Allow-Headers': 'X-REQUESTED-WITH, CACHE-CONTROL, LAST-EVENT-ID',
        'Content-Type': 'text/plain'
    })
    return ''

In case you’re curious, those are the headers the 4.2 stock WebView requires for EventSource to work in this scenario.

The Side of Things

That’s a bit out of scope here, but here’s the gist of things:

function init(conf, app) {
    var source = new EventSource(conf.url)

    if (conf.debug) console.log("Binding event source");

    source.addEventListener('init', function(e) {
        app.trigger("model:init", e.data);
    }, false);

    source.addEventListener('delta', function(e) {
        app.trigger("view:update", e.data);
    }, false);

    source.addEventListener('error', function(e) {
        if (e.readyState == EventSource.CLOSED) {
            app.trigger("view:networkError");
        }
        else if( e.readyState == EventSource.OPEN) {
            app.trigger("feedback:connecting");
        }
    }, false);
}

As you can see, it’s mostly a matter of matching server-sent events to my single-page app triggers.

The above is using RiotJS, which I’m rather partial to these days because I can build a simple single-page app using it and Zepto with templating, a sane MVC approach and observables that fits completely under 50KB of code2, so I’m sticking to it for simple, elegant stuff.

And that’s it for tonight. Next up, functional patterns or sane approaches to threading, depending on what I have on my plate.


  1. Mind you, these days there are great solutions like the nginx Push Stream Module, but it’s a bit overkill. ↩︎

  2. The fact that this approach requires exactly zero module management or build tools (other than a bundler/minifier, which is de rigueur anyway) is just icing on the cake. ↩︎