The power of HTTP for REST APIs – Part 2

In part 1, we looked at the fundamentals of HTTP: URLs, methods, response codes, and headers. We then applied these powerful concepts into content negotiation, language negotiation, and hypermedia/HATEOS, allowing our APIs to offer more robust capabilities without affecting our API design.

In this article, we will build on these foundational concepts to extend our understanding further and better understand how we can add caching and concurrency control to our APIs – simply by using what is built-in to the HTTP specification.

Client-side caching: Improving app performance

A cache is a local store of data to prevent re-retrieval of the data in the future. Developers familiar with the term have likely used server-side caching use tools such as memcached to keep data in memory and reduce the need to fetch unchanged data from a database to improve application performance.

HTTP cache semantics allow for cacheable responses to be stored locally by clients, moving the cache away from the server-side and closer to the client for better performance and reduced network dependence.

To support this, HTTP makes available several caching options through the Cache-Control response header that defines if the response is cacheable and, if so, for how long. Responses may only be cached if the HTTP method is GET or HEAD and the proper Cache-Control header indicates the content is cacheable.

Let’s re-examine our content negotiation example request:

GET https://api.example.com/projects HTTP/1.0

Accept: application/json;q=0.5,application/xml;q=1.0


Below is an example response that includes a caching directive from the API server:

HTTP/1.0 200 OK

Date: Tue, 16 June 2015 06:57:43 GMT

Content-Type: application/xml

Cache-Control: max-age=240



<project>...</project>


In this example, the max age indicates that the data may be cached for up to 240 seconds (4 min) before the client should consider the data stale.

The Google Developer website has a great article by Ilya Grigorik that details the client-server interactions of HTTP caching.

Summary: HTTP provides a cache-control header that informs clients if a response is cacheable and for how long. Applying this header to our API client code will reduce network traffic and speed up our web and mobile applications. Thoughtful caching design is required to take advantage of these capabilities offered by HTTP, including what resources are cacheable and for how long.

Intermediary caching: Reducing network latency

The HTTP spec defines support for intermediaries, allowing requests to be processed by a chain of connections. These intermediaries are placed between the client and API server as needed, enabling network behaviour be added without the APIs knowledge. Examples of intermediary usage include:

  • a reverse proxy/gateway used for routing to services inside the firewall and enforcing security/rate limiting
  • a web application firewall to protect against common attack vectors, such as XML parser attacks, SQL injection, etc.
  • generate metrics and analytics of incoming requests to provide teams with insights into what endpoints are used, if any are returning excessive error response codes, etc.
  • caching servers that can return responses back to the client quickly and without involving a backend service, such as those provided by Varnish and nginx+Redis

The last point listed above, caching servers, are powerful. They sit in front of an API that provides Cache-Control response directives and store the responses on behalf of an API server. This frees API clients from implement caching support while providing transparent app acceleration when placed in front of an API backend.

The further away the caching server is from the client application, the more time it will take to make a round-trip API request. This is where content distribution networks (“CDNs”) help, as they place caching servers all around the world to be closer to client applications. Let’s look at an example:

A mobile application connecting from London to an API server in Australia may transparently connect through a CDN edge node and to the remote API server on the first request. The CDN edge node then caches the API server response and returns the result back to the API client. Subsequent requests will be serviced by the CDN edge node that is closer to the mobile app – until the cache expiration time has been exceeded and the CDN edge node needs to refresh its cache from the API server.

Fastly has published a nice article on API caching that provides a detailed example of how it works, along with some of the backend cache control techniques used to invalidate CDN caches when backend data has changed.

It is important to note that when using caching servers, the client application still must make a network connection to the caching server to refresh data.  To avoid the network round trip completely, client-side caching may be used to store the data within the application itself. In combination, we reduce network round trips when the are unnecessary, but benefit from caching servers when we need to refresh our client-side cache.

Summary: HTTP supports intermediaries between an API client and server. They understand the HTTP protocol and can assess incoming requests for cacheability without the need to parse or understand the specific request/response payload.

Conditional requests: Staying up-to-date

Conditional requests are a lesser known but powerful capability offered by HTTP. Conditional requests allow clients to request an updated resource representation only if something has changed. Clients that send a conditional request will either receive a 304 Not Modified if the content has not changed, or 200 OK along with the changed content. There are two options for telling the server about the client’s local cached copy for comparison: eTags and time-based.

The entity tag, or “eTag”, is an opaque value that represents the current resource state. The client may store the eTag after a GET, POST, or PUT request and use the value to check for changes to the representation in the future via a HEAD request. Commonly, the eTag is a hashed value of the state, although this is not a requirement. All that is required is a way for the server generate a unique eTag value that can be used to determine if the state has been modified since it was last retrieved:

200 OK

Location: /projects/12345

Cache-Control: public, max-age=31536000

ETag: "17f0fff99ed5aae4edffdd6496d7131f"

The client may then use the If-None-Match request header to indicate the last eTag received:

GET /projects/12345

If-None-Match: "17f0fff99ed5aae4edffdd6496d7131f"

Alternatively, we can use time-based preconditions with the Last-Modified response header. The If-Modified-Since request header can then be used to specify the last updated timestamp to compare against the last update timestamp on the server to see if anything has changed:

200 OK

Location: /projects/12345

Cache-Control: public, max-age=31536000

Last-Modified: Mon, 19 Mar 2018 17:45:57 GMT

When the API client sends a GET request, it includes the last modified timestamp as part of the conditional request:

GET /projects/12345

If-Modified-Since: Mon, 19 Mar 2018 17:45:57 GMT

If the resource hasn’t changed since Mon, 19 Mar 2018 17:45:57 GMT, then the client will receive a 304 Not Modified rather than a 200 OK with the latest resource representation.

Summary: Conditional requests reduce the effort required to validate and refetch cached resources. eTags are opaque values that represent the current internal state, while last modified timestamps may be used for date-based comparison rather than eTags. We use the appropriate precondition request header to inform the server of the version on the client. The API server then returns the latest representation of the resource, or a 304 Not Modified if nothing has changed since the last fetch.

Concurrency control: Protecting resource integrity

Conditional requests are also used to support concurrency control. By combining eTags or last modified dates with state change methods (e.g. PUT), we can ensure that data is not overwritten accidentally by another API client. This is especially important in the case of a PUT method, where the entire resource representation is replaced by a new representation provided by the client.

When an API client issues a modifying request, they may add a precondition to the request to prevent modification if the eTag has changed (via the If-Match request header) or if the timestamp has not changed (via the If-Unmodified-Since request header). Should the precondition fail, a 412 Precondition Failed response is sent by the server. Servers may also enforce the requirement of a precondition header to enforce concurrency control by responding with a 428 Precondition Required if neither of these request headers were found.

Let’s take an example where two API clients are trying to modify a project. First, each client retrieves the client using a GET request, obtaining the following response:

Location: /projects/12345

Cache-Control: public, max-age=31536000

ETag: "27f0fff99ed5aae4edffdd6496d7131f"



{...}

The first API client then modifies the project by replacing the current representation with a new one:

PUT /projects/1234

If-Match: "27f0fff99ed5aae4edffdd6496d7131f"



{ "name":"Project 1234", "Description":"My project" }

The server responds with a 200 OK, since the eTag matches and returns an updated representation with a new eTag:

200 OK

Location: /projects/12345

Cache-Control: public, max-age=31536000

ETag: "57f0fff99ed5aae4edffdd6496d7131f"



{...}

The second API client, which has the same eTag when it originally fetched the project, is trying to modify the project as well:

PUT /projects/1234

If-Match: "27f0fff99ed5aae4edffdd6496d7131f"



{ "name":"Project ABCDE", "Description":"My renamed project" }

Unfortunately, the project has changed, so the second API client receives a different response:

412 Precondition Failed

The second API client must now re-fetch the current representation of the resource instance, then inform the user of the changes and allow them to determine if they wish to re-submit the changes made or leave it as-is.

Summary: Concurrency control may be added to an API through HTTP preconditions in the request header. If the eTag/last modified date hasn’t changed, then the request is processed normally. If it has changed, a 412 response code is returned, preventing the client from overwriting data as a result of two separate clients modifying the same resource concurrently.

Wrap-up: Part 2

HTTP is a powerful protocol with a robust set of capabilities. In this article, we examined caching directives, how these directives can drive client-side and intermediary caching options, and how HTTP preconditions can be used to determine if expired caches are still valid while protecting resources from concurrent modification. By applying these techniques, we can build robust APIs that drive complex applications that are both resilient and evolvable.

Special thanks to Darrel Miller and Matthew Reinbold for reviewing this article.