Yoong Kang Lim

Writing an ASGI web framework

Earlier this week, I played around with WSGI to answer a question I had. In my experimentation, I wrote a bunch of boilerplate (badly) to handle HTTP.

This then led me to another question, which was “okay, how hard would it be to port this stuff to ASGI”?

And then I went down a bit of a rabbit hole.

ASGI

In a nutshell, ASGI is meant to be the spiritual successor of WSGI for asynchronous applications. That means you can write async code, and it can support protocols like WebSockets.

It was borne out of the work done to build Django Channels.

ASGI was something I kind of ignored for a long time. I initially thought it was something only relevant to Channels, another thing I kind of ignored for a long time.

A couple of years back I tried reading documentation and some talks on Channels, but honestly didn’t really grok it. It was kind of something that I think I would only need if I wanted WebSockets, and if I also didn’t want to have a Node service running.

I don’t have any particular aversion to writing JavaScript, and writing JavaScript seemed like the path of lower resistance of the two at the time. Channels seemed to have too many new things for me to learn (asyncio, Daphne, Channels), and the API looked like it needed some time to mature.

The alternative was writing a Node.js script with socket.io as a dependency, maybe using Redis as a message bus. That was more accessible for me.

(At this point, I think it might be good to disclose that I haven’t used WebSockets or Channels much, so I might very well be talking out of my ass. Take the above with a grain of salt.)

Anyway, a couple of years later ASGI is now at version 2.0, Channels is at 2.0, and Daphne is also 2.0. As I ignored most of version 1.0, there was nothing for me to “relearn”, which is a pretty good place to be in.

Apparently, things are now quite exciting. A number of ASGI-compliant web servers apart from Daphne have also popped up, with a few web frameworks. It’s no longer just about WebSockets.

Async is fast

We’re early days, but a few benchmarks indicate that the speed gains are noteworthy.

My colleague at Airteam, Jordan Eremieff made some benchmarks by getting Django running on ASGI, and the results were quite motivational:

By the way, Jordan has been doing some great work on ASGI, including a micro-framework called Afiqah and an ASGI web server called Fikki.

There are also benchmarks on TechEmpower:

Tom Christie wrote the Uvicorn web server, which is an ASGI-compliant web server.

How does ASGI work?

ASGI is an interface.

This is the interface (recommended in the docs):

class Application:
    def __init__(self, scope):
        ...
    async def __call__(self, receive, send)
        ...

It’s a callable that takes a “scope” (more on that later), and returns a coroutine callable.

My initial reaction was that it seemed like a strange way to express a closure, but I guess there are benefits writing it this way. It’s easier to manage state and write helper methods, and it also allows you to do inheritance.

Otherwise, if you prefer a more functional style (like me) you could write the same thing this way:

def app(scope):
    async def inner(receive, send):
        ...
    return inner

The two arguments the coroutine callable expects, receive and send are themselves coroutine callables.

As their names suggest, it’s a layer for bidirectional communication with your protocol.

Let’s talk about what happens for a particular protocol, HTTP (Hyper Text Transfer Protocol).

When an ASGI-compliant server receives an HTTP request, it passes a particular scope to the ASGI application.

The format of the scope for HTTP requests is something like this (taken from the Uvicorn docs):

{
    'type': 'http',
    'scheme': 'http',
    'root_path': '',
    'server': ('127.0.0.1', 8000),
    'http_version': '1.1',
    'method': 'GET',
    'path': '/',
    'headers': [
        [b'host', b'127.0.0.1:8000'],
        [b'accept', b'*/*']
    ]
}

That means you can take this scope and decide what messages you want to send.

Here’s an example:

def app(scope):
    async def inner(receive, send):
        if scope['path'] == '/':
            await send({
                'type': 'http.response.start',
                'status': 200,
                'headers': [
                    [b'content-type', b'text/html']
                ]
            })
            await send({
                'type': 'http.response.body',
                'body': b'<h1>Index page</h1>'
            })
        elif scope['path'] == '/hello':
            ...
        else:
            ...
    return inner

Just by following this pattern, we can write entire web applications using asynchronous Python.

That’s a lot of boilerplate to write, though. It’s pretty annoying to deal with the raw send and receive ASGI gives you, so the way to go is really to create wrapper Request and Response objects.

Basically, you’ll want to write a framework around this.

Writing a framework

Some people like to say this about the framework/library distinction: your code calls libraries, while frameworks call your code.

That’s true, but I would also like to go one step further and say frameworks also recommend an API for you to write code.

I knew I wanted a good API.

So instead of inventing my own API from scratch, I decided to take inspiration from what’s already successful since it works for many people.

The Node community has been writing async web applications for a while now, and the most successful framework is Express.

If you’re unfamiliar with Express, this is what it looks like:

const express = require('express')
const bodyParser = require('body-parser')

let app = express()

app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())

const index = (req, res) => {
    res.send('<h1>hello world!</h1>')
}

const hello = (req, res) => {
    const name = req.body.name;
    res.send(`<h1>hello, ${name}</h1>`)
}

app.get('/', index)
app.get('/hello/:name/', hello)

app.listen(3000, () => console.log('Example app listening on port 3000!'))

I decided to borrow that API, since it’s quite nice. I was thinking of eventually having something like this:

from matchers import get
from asgi import main

async def index(req, res):
    await res.send("<h1>Index</h1>")

async def hello(req, res):
    name = req.params['name']
    await res.send(f"<h1>Hello, {name}</h1>")

routes = [
    get('/', index),
    get('/hello/:name/', hello),
]

# this is the ASGI application
app = main({
    'routes': routes,
})

This is quite different from the Django-style API where a “view” (not crazy about that name) takes a request object, and returns a response object.

In async code we may potentially split the response into several chunks. Django does have a StreamingHTTPResponse class which takes an iterator, and we can do the same thing without expanding the API.

I think we could also receive incoming HTTP request data in chunks, but I’m not sure yet if this is a worthwhile API to provide.

After about two evenings I think I arrived at something pretty close.

Nardis

I ended up with something I called Nardis.

You can try the framework out as I’ve pushed it to PyPI. Just install it via pip like this:

$ pip install nardis

However, I recommend building from source to get the latest code.

The API currently is slightly different to the example above, but is pretty close.

The main difference is that it currently uses regex-style routing, like older versions of Django, but you can write your own pattern matching class.

An example application looks like this:

from nardis.asgi import main
from nardis.routing import Get as get, Post as post

async def index(req, res):
    await res.send("<h1>Index</h1>")

async def hello(req, res):
    name = req.params['name']
    await res.send(f"<h1>Hello, {name}</h1>")

routes = [
    get(r'^/?$', index),
    get(r'^/hello/(?P<name>\w+)/?$', hello),
]

# this is the ASGI application
app = main({
    'routes': routes,
})

You can also look at the source code on the GitHub repo.

Choosing the name “Nardis”

I’d like to digress and talk about the name Nardis for a moment.

The Python web development community has a tradition of naming the frameworks and libraries after jazz musicians. Django is named after Django Reinhardt, and there are libraries like Grappeli (Stephane Grappelli, a frequent collaborator of Reinhardt), Satchmo (Louis Armstrong’s nickname).

I think it’s well known to most people who know me that I listen to a lot of jazz, so I’m all too happy to keep this tradition going.

Initially, I wanted to call my framework “Trane” (after John Coltrane), but that was already taken on PyPI. Bird, Dizzy, and Miles are all taken. Thelonious is available (as of writing) but is too long.

After a few names, I gave up and tried to think of jazz song titles instead. I’m not going to call my framework “straightnochaser” or “roundmidnight”, so it has to be a shorter title that nobody would take.

I did a search for “Nardis” and was delighted that nobody had taken it.

Nardis is a really awesome tune, written by Miles Davis and popularised by Bill Evans.

Watch Bill Evans absolutely shredding the tune here:

It’s a mystery what “Nardis” actually means, but there are several theories (opens as PDF).

If you’re interested in the history of the tune, be sure to read this great blog post!

Next steps

There is still a lot of work I need to do before I’m comfortable releasing a version 1.0 for Nardis.

It doesn’t make sense to provide everything, but at the minimum I want to figure out the best way to allow Nardis to work with ASGI middleware. This should allow people to write middleware for things like authentication, and file uploads (!).

If it surprises you that the file upload capability isn’t implemented natively, bear in mind Express doesn’t have an API for files in their request object either. To be more accurate, Express used to have this API, but it is now provided by middleware. That seems like a cleaner way to go about it.

I would also love feedback on the API, so if you’re interested at all in what I’m doing please get in touch!

The rest of the ASGI ecosystem

It’s still early days, but there are a number of frameworks/web servers that support ASGI.

Here’s a non-exhaustive list:

Web servers:

Frameworks: