Another PUSH defense: APIs

Hi everyone,

I wanted to chime into the discussion and share a different use-case for
h2 push.

My background is mostly API development. In the last few years I've
developed a number of REST-flavored services, most recently using the
HAL[1] format.

When looking at various hypermedia formats, they all tend to have
something in common, the need to embed resources.

What this generally means, is that if you do a GET request on a
'collection', the response typically gives you information about the
collection, but also every entry inside of it. If effectively embeds the
representation you would have gotten if you had done a GET request
directly on individual item in the collection.

Every one of those embedded GET responses has their own url, their own
contents and their own list of links.

* HAL does this with _embedded.
* Atom[2] does this with the "item" element
* Collection+JSON[3] does this with the "items" JSON property
* Siren[4] does this with the "entities" JSON property
* JSON API[5] does this via the "relationships" property

Here's an generic example of a fictional collection in a fictional
hypermedia format.

HTTP/1.1 200 OK
Content-Type: application/json

{
  links: [
     { href: '/collection/1', rel: 'item' },
     { href: '/collection/2', rel: 'item' },
     { href: '/collection/3', rel: 'item' },
     { href: '/collection/4', rel: 'item' },
     { href: '/collection/5', rel: 'item' },
     { href: '/collection/6', rel: 'item' },
  ],
  total: 6,
  embedded: {
    '/collection/1' : {
       links: { ... },
       ...other properties
    },
    '/collection/2' : {
       links: { ... },
       ...other properties
    },
    '/collection/3' : {
       links: { ... },
       ...other properties
    },
  }
}

Chances are that for a given 'hypermedia' format, you will find some
mechanism to embed resources.

To me, all of this is an anti-pattern, to work around a limitation of
HTTP. It's cheaper to do a single GET request and get _many_ HTTP
responses embedded in one, than doing many GET requests for each item in
a collection. It's a sane design for HTTP/1.1.

The HAL draft has a good explanation of this in it's draft. The section
is titled "Hypertext cache pattern"[6].

The problem is also apparent in non-hypermedia formats. GraphQL for
example cites on their website[7] the following 2 main advantages over
using resource-oriented architectures:

1. Ask for what you need, get exactly that
2. Get many resources in a single request

In particular selling point #2 is interesting. Not many people would
care about 'getting many resources in a single request' or even
intuitively think that getting multiple answers to a single question is
a good thing if it weren't for the fact that we're conditioned that less
HTTP requests has been a good thing.

However, doing many GET requests is semantically better. Some of the
issues with getting collections of embedded responses are:

* HTTP caches completely ignore them. It's not uncommon for client
  implementations of API specifications to re-create their own userland
  cache that's only ware of 1 specific format.
* This also means that HTTP caches for individual items don't
  automatically invalidate when receiving a new version of an embedded
  resource.
* It's typically not possible to embed items that are of a different
  media types than their parent. If your parent resource is encoded as
  JSON chances are the embedded item probably must too.
* Even though embedded items represent 'what you would have gotten if
  you did a GET request directly on the item', they typically miss
  HTTP response headers.
* It's often unclear how applications should behave when receiving
  embedded resources from other origins.

So taken all these things into consideration, I wondered if it was
possible to create a generic format for embedding resources in other
(JSON resources). I realized that this format would need to at least
have the following properties:

* Be generic enough so it can be used for different API specifications.
* Embed HTTP response headers for each item.
* Embed some HTTP request headers for each item, so a generic cache
  could pick this up. See RFC7234, section 4.1 "Secondary cache keys"[8]
* Have defined semantics for cross-origin embeds.

When I listed these requirements it occurred me that HTTP/2 already has
a protocol-level facility for this: PUSH and the ORIGIN[9] frame.

My hope for the future of REST apis is this:

1. When HTTP requests are cheaper, entities can be smaller.
2. Instead of non-semantically nesting HTTP responses in HTTP
   responses, related resources can be pushed.
3. The ORIGIN frame is used for pushing resources from a collection
   of other (controlled) origins.
4. Cache Digests[10] and ETags are used to only push resources the
   client needs.

#4 really makes this the killer feature, because it doesn't just make
this incrementally better, it actually will have major performance
benefits. Then if developers can stop building their own caches, you get
a lot of speed for free.

So my plea to you is to don't kill push just yet. We're still missing
some of the building blocks (cache digests), but with the right
framework in place, I think we can see a new wave of API designs that
weren't viable before.

Evert

[1]: http://stateless.co/hal_specification.html
[2]: https://tools.ietf.org/html/rfc4287
[3]: http://amundsen.com/media-types/collection/format/
[4]: https://github.com/kevinswiber/siren
[5]: http://jsonapi.org/
[6]: https://tools.ietf.org/html/draft-kelly-json-hal-08#section-8.3
[7]: https://tools.ietf.org/html/rfc7234#section-4.1
[8]: https://tools.ietf.org/html/rfc8336
[9]: https://tools.ietf.org/html/draft-ietf-httpbis-cache-digest
[10]: https://graphql.org/

Received on Saturday, 1 September 2018 11:48:48 UTC