Or, what to do about HTTP 202

Sometimes, we write code that takes a while to run. Whether that’s because of a big linear optimization problem (it could take minutes!) or because of external HTTP calls (it could take seconds!), it would increase the latency of our HTTP responses unacceptably, and so we have to push it out of band; you push the task onto a job queue, handle it in the background, and return to the client a nice snappy HTTP 202.

Uh oh. HTTP 202. Let’s read RFC7231, Section 6.3.3:

The 202 (Accepted) status code indicates that the request has been accepted for processing, but the processing has not been completed. The request might or might not eventually be acted upon, as it might be disallowed when processing actually takes place. There is no facility in HTTP for re-sending a status code from an asynchronous operation.

The 202 response is intentionally noncommittal. Its purpose is to allow a server to accept a request for some other process (perhaps a batch-oriented process that is only run once per day) without requiring that the user agent’s connection to the server persist until the process is completed. The representation sent with this response ought to describe the request’s current status and point to (or embed) a status monitor that can provide the user with an estimate of when the request will be fulfilled.

“A status monitor”? What, like, some endpoint you have to poll? Nah.
This is 2019, and we can do better. We have websockets. We’ve had them
as a standard for the better part of a decade, in fact.

Let’s talk about how we can use websockets to publish updates in an
otherwise RESTful API environment.

The use-case

Imagine this: we have a resource, let’s call it a Fluxit, that
unfortunately takes a long time to create, update, and delete. It’s
got some complex calculations on create and update, and it has to make
an external HTTP call to delete a resource on someone else’s API on
delete. It’s the worst of all worlds, but we have to handle it.

So our RESTful API looks like this:

GET .../fluxits/
   return a list of all Fluxits, with code 200
POST .../fluxits/
   eventually create a Fluxit, with code 202
GET .../fluxits/:id
   return a single Fluxit, with code 200
PUT .../fluxits/:id
   eventually update a Fluxit, with code 202
DELETE .../fluxits/:id
   eventually delete a Fluxit, with code 202

We can’t write a user-facing client for this without much wailing and
gnashing of teeth. Three of five endpoints don’t tell you whether they
worked or not, just that they might, eventually, work.

(Note, I’m not including the GET requests in this 202 nightmare
because I think it’s a lot less likely that a GET would be so slow
as to warrant it, and if it is, you should probably fix your backend
architecture to speed it up. That’s not a problem to solve with
websockets, it’s a problem to solve with database design.)

What I would like, as someone writing the client consuming this API, is
to subscribe to a websocket that will tell me when my
POST/PUT/DELETE has finished, and with what status. I can
lock the client UI or leave it open as-called-for, but I need to know
when things are in a completed state.

Aside: What is this, anyway?

REST isn’t a standard, it’s a style. A style is valuable particularly as
a way to set consistent terminology and expectations for developers to
use. This makes it easier to use a new API because you can readily fit
it into a mental framework.

So that’s what I’m proposing for adding push to a RESTful API: a style, nothing more. Something you can use, say you’re using, and help others interact with because they know it by name.

A style only works if people adopt it, though, so I want this to work
for you, and be something you use and talk about. We’ll have to work
together to make a pattern worth using!

Back to it: Publishing changes to resources, version 1

OK, so how do we know when our Fluxits have completed their slow
operations? Well, the 202 response could be something like the
following:

HTTP/1.1 202 ACCEPTED
Content-Type: application/x-restsub+json
Link: <wss://api.example.com/fluxits/:id>

{
  "notifications": "wss://api.example.com/fluxits/:id"
}

And so our client can connect to that websocket and await events over
it. Those events are going to be JSON payloads that imitate what we
might get from an HTTP response. Let’s say we did a PUT and are
waiting for an update to complete. When it does, we get the following
over the websocket:

{
  "header": {
    "method": "UPDATE",
    "uri": "https://api.example.com/fluxits/:id"
  },
  "body": {
    "id": "asdf4",
    "title": "I am a banana",
    "content": "When one sees a banana, one becomes a banana."
  }
}

The returned JSON should have two keys, "header" and "body.
"header" should contain at least "method" (which should be one
of "CREATE", "UPDATE", or "DELETE"), and "uri", which
should be the canonical identifying URI for the resource. "body"
should contain the same JSON that would be returned if one did a GET
on the resource at that very moment.

On special case is the DELETE, event where you’d expect a matching
GET to return a 404; in this case "method" and "uri" should
still be present in the header, but the body can and should be an empty
object: {}.

Publishing changes to resources, version 2

What we just did works pretty well, but it has a couple problems!

  1. What if we want to be notified of any new Fluxits in the
    collection, not just one we kicked off?
  2. What if the “slow” processes are sometimes actually very quick, and
    complete before we can subscribe to the websocket? We don’t want to
    be let hanging!
  3. What if we want to be notified about many resources, and don’t want
    to maintain a separate websocket for each one?

It turns out that all of these problems interact, and can be addressed
together.

Let’s go backwards and start by solving problem 3. Instead of opening a
websocket for each resource we want to subscribe to, what if our API
offers one big firehose of notifications, at something like
wss://api.example.com/notifications? We can send “subscribe”
requests over this websocket, and receive confirmations or rejections of
those subscriptions, followed by events on that resource when we have a
successful active subscription to it. We can also then send an
“unsubscribe” action to stop listening to select resources, or just
close the websocket if we want to unsubscribe from everything.

Now this implies a solution for problem 2! We can subscribe to a
resource before we even make a request to it. We can subscribe to the
individual resource before we PUT or DELETE it, since we have
its identifying URI, and we can subscribe to the collection before we
POST a new one. So now we’ll never miss a websocket event, and have
one fewer failure case to code around.

And this, in turn, implies a solution to problem 1. If we can subscribe
to collections, and not just individual resources, we can listen for any
new Fluxit CREATE events the server wants to send us.

So we’ve just backed our way into a justification for this approach. A REST-push API should provide a notifications firehose websocket endpoint, over which clients can subscribe to the particular resources they care about.

What do these “subscribe” and “unsubscribe” websocket messages look
like, and what do the “success” and “failure” control messages look
like? As much like normal HTTP requests as we can manage!

Perhaps like this:

{
  "header": {
    "method": "SUBSCRIBE",
    "uri": "https://api.example.com/fluxits/:id"
  }
}

and:

{
  "header": {
    "method": "UNSUBSCRIBE",
    "uri": "https://api.example.com/fluxits/:id"
  }
}

and the responses look like:

{
  "header": {
    "method": "SUBSCRIBE",
    "uri": "https://api.example.com/fluxits/:id",
    "status": 200
  }
}

with appropriate changes to "method" and "status" in the case of
unsubscription and errors. The server can also force-unsubscribe the
client from resources, for example when that resource is deleted.

Aside: Showing clients what they need to see

A server can send different messages to different clients, maintaining
the consistency of their own understanding of the resources. If a
client’s permission to see a resource has been revoked, you can send
them a "DELETE" event, while others may just receive an "UPDATE"
event, or nothing, depending on whether there are any changes they can
see. Similarly, if you are subscribed to a collection and it is filtered
down to hide certain resources from you, and one changes to be visible,
you should get a "CREATE" event for that resource when it shows up.

Aside: Linking requests and events

Sometimes, you might need to queue up a bunch of tasks, and want to know
the order of the related events relative to those queued tasks. Imagine
we send three updates, and want to discard any websocket events that are
out-of-order. We can add a unique ID or a timestamp or something to the
HTTP response, and then include the same ID in the websocket event from
the server. This isn’t a use-case I’ve had yet, so this is no more than
a notion, but maybe worth thinking about!

Or the client can generate its own IDs (let’s say UUIDs) and that would
get us closer to implementing a vector clock, I hear. I don’t know! I
welcome additional thoughts on this.

Putting it all together

So let’s walk through an example!

My single-page app JS client decides it wants to know about any changes
to the Fluxit collection, and so I create a websocket connection to
wss://api.example.com/notifications, and send the following
subscription message over it:

{
  "header": {
    "method": "SUBSCRIBE",
    "uri": "https://api.example.com/fluxits",
  }
}

The server responds over the websocket:

{
  "header": {
    "method": "SUBSCRIBE",
    "uri": "https://api.example.com/fluxits",
    "status": 200
  }
}

We’re good to go!

As we use the app, we’ll occasionally create Fluxits, by sending normal
Ajax requests, like so:

POST /fluxits HTTP/1.1
Host: api.example.com
X-Example-Api-Id: 82195fa7-ccab-494a-b570-ccb4461d1933

{
  "title": "My Fluxit",
  "description": "This is the best Fluxit yet!"
}

and getting a response like:

HTTP/1.1 202 ACCEPTED
Content-Type: application/x-restsub+json
Link: <wss://api.example.com/notifications>

{
  "notifications": "wss://api.example.com/notifications"
}

But we can ignore everything but the 202 status code, because we’ve
already subscribed to the Fluxit collection at that notifications
endpoint.

Then, a couple seconds later, a websocket message comes down from the
server:

{
  "header": {
    "method": "CREATE",
    "uri": "https://api.example.com/fluxits/asdf4",
    "x-example-api-id": "82195fa7-ccab-494a-b570-ccb4461d1933",
    "status": 201
  },
  "body": {
    "id": "asdf4",
    "title": "My Fluxit",
    "description": "This is the best Fluxit yet!",
    "expensive_computed_value": 42
  }
}

and we can add it to our local datastore (Redux, Vuex, whatever we use).

(Perhaps, if we control the server and the client, we also add a value
in the "header" key that maps to our Redux actions, like "action":
"CREATE_FLUXIT"
and that lets us short-cut some client-side logic to
figure out what sort of Redux action to take.)

This can also play well across clients, of course. Say our best friend
Alice makes another Fluxit that we should see. We can just as easily
receive the same sort of websocket event, as we’re subscribed to the
collection.

Now, what if we try to make a malformed Fluxit? We do a POST like
this:

POST /fluxits HTTP/1.1
Host: api.example.com
X-Example-Api-Id: 0bcc58b9-a595-4520-9597-4c4fef1ff895

{
  "title": "My very bad Fluxit has no description"
}

and, of course, get a response like:

HTTP/1.1 202 ACCEPTED
Content-Type: application/x-restsub+json
Link: <wss://api.example.com/notifications>

{
  "notifications": "wss://api.example.com/notifications"
}

Depending on our need, we can fake-make the Fluxit locally, or throw up
a spinner, or just let the user get back to their other interactions,
but then a few seconds later we get this over the websocket:

{
  "header": {
    "method": "CREATE",
    "uri": "https://api.example.com/fluxits/asdf4",
    "x-example-api-id": "0bcc58b9-a595-4520-9597-4c4fef1ff895",
    "status": 422
  },
  "body": {
    "errors": {
      "description": [
        {"message": "This field is required."}
      ]
    }
  }
}

And so on, and so on.

Final thoughts

I hope this is useful and provokes some thought on the matter! I’ve been
making a Python library to make this easy to handle from the server side
assuming you’re using Django REST Framework and Django Channels, so if
that’s your situation, ask me about it and maybe try it out!

This is inspired by WebSub, but geared towards client applications, not
webhook-driven inter-server communication. It’s a topic I’ve had to work
with, but definitely don’t have a formal understanding of, so I welcome
corrections and additions.