mnot’s blog

Design depends largely on constraints.” — Charles Eames

Wednesday, 11 July 2012

Bad HTTP API Smells: Version Headers

One thing I didn't cover in my previous rant on HTTP API versioning is an anti-pattern that I'm seeing a disturbing number of APIs adopt; using a HTTP header to indicate the overall version of the API in use. Examples include CIMI, CDMI, GData and I'm sure many more.

These APIs are designed so that the client can send a request header advertising what version of the protocol they support:

GET /foo HTTP/1.1
BadApiVersion: 1.2

and the server will advertise what version it supports in the response:

HTTP/1.1 200 OK
BadAPIVersion: 1.1

This is broken and wrong for a whole mess of reasons. Why?

First, because the server's response depends on the version in the request header, it means that the response really needs to be:

HTTP/1.1 200 OK
BadAPIVersion: 1.1
Vary: BadAPIVersion

because otherwise, intervening caches can give clients the wrong response (e.g., a 1.2 response to a 1.1 client, or vice versa).

(You say you won't be using caches? Really? What will do you do when your load spikes and your API servers can't take it? Besides which, it's an open secret that big API clients commonly cache their calls, to avoid being rate limited as well as to recover more gracefully from service outages. You want to mess them up?)

Treating versioning like content negotiation also means that you need to define how servers should handle requests with versions that they don't understand, as well as requests that don't have a version. What happens if the version changes halfway through an interaction, by the way?

The big problem, though, is that putting the version into a header implies that all of your changes will still use the same URIs and media types; otherwise, why not just use new ones? 

Think of it this way: HTTP gives you a number of ways to introduce both compatible and incompatible changes, primarily using new resources and new media types. While adding a version header to this mix might seem to leave your options open, it's actually a bad API smell that indicates a tendency to treat HTTP more like RPC, a la SOAP.

So, What's the Right Way?

From what I can tell, protocol designers use a version header out of habit, more than anything; they're so used to versioning, and they're creating a new protocol, so it must have a version, right?

Identifying a bag of functionality that fits together is a useful thing to do; it's just that putting it in a header on every request and every response is overkill and too limiting. 

Instead, a protocol should define its resources and formats, and then identify how they fit together as a protocol and give that set of functionality a separate version, if necessary.

For example, if I'm designing FooProtocol1 to use HTTP, I might have an "application/foo" media type and a "fooHolder" resource. However, the versioning of all three things -- the resource, the format it uses, and the protocol that utilises them (and presumably a bunch of other things) -- are separate, in that they can evolve separately.

In this manner, "fooHolder2" can choose to continue to use "application/foo", or "fooHolder" can decide to start supporting "application/bar" as well. FooProtocol1 acts as a "profile" of the resources and formats, to promote interoperability. When enough changes to the formats and resources happen, you can mint a FooProtocol2.

I can hear some people asking where they discover that the server supports FooProtocol1 or FooProtocol2. In reality, they shouldn't have to; the server should advertise the resources and formats they support, and the client can pick and choose from them. 

I'll write more about how that discovery might happen soon (hint).

Filed under: HTTP Protocol Design


Michael Hart said:

A lot of libraries take this approach too, including support for semver:

Can you expand a bit more on what's wrong with this approach? ie, give a concrete example of what may actually happen?

Wednesday, July 11 2012 at 2:28 PM +10:00

Leave a comment

Creative Commons