ETags With Laravel Filters

ETags PHONE HOME?! No. So what are ETags? Wikipedia has this to say:

The ETag or entity tag is part of HTTP, the protocol for the World Wide Web. It is one of several mechanisms that HTTP provides for web cache validation, and which allows a client to make conditional requests. This allows caches to be more efficient, and saves bandwidth, as a web server does not need to send a full response if the content has not changed. ETags can also be used for optimistic concurrency control as a way to help prevent simultaneous updates of a resource from overwriting each other.

Often my wife will call me up and ask me to read her the grocery list because she forgot it. So I waddle over to the refrigerator and start reading off the list which can take several minutes if it’s large. Once she has this, she is done. In this case, I am the server, she is the client. Now imagine if my wife called me up 2 minutes later and asked the same question? Would I go back to the refrigerator and spend another 5 minutes reading off a list? No, I’d tell my wife:

304 - Not Modified.

The grocery list hasn’t changed in the last two minutes. There is no reason to read it off again. But there is a problem. I can’t just ignore the fact that my wife is calling me again by instantly hanging up on her just because the list hasn’t changed.

  • Maybe she lost the list?
  • Maybe she wants to verify the list hasn’t changed?
  • Maybe the dog ate her list?
  • Maybe she just wants to say hi?

This is a list we are talking about. Things are written in order. Perhaps she could just tell me the last thing on her list and if it matches the last item on mine then we are good to go. Perhaps we could also compare the number of items in the list.

Or we could use an ETag.

Let’s switch context over to the world wide web. If I send some html, json or xml to a client then if they ask for the same page again within a “time-frame” I am just going to tell them to use the data they already have in front of them.

In the above example we used “time” as a factor for our ETag. The grocery list isn’t going to change every hour. We might update it a few times throughout the week. The combination of current date + the resource (we will use url) becomes the entity we are tagging.

When the server and client have to communicate, it is costing time (and time is money) to both parties involved.

  1. The server has to spend time conjuring up data for the client.
  2. The client has to spend time capturing the data sent from the server.

Let’s look at how we can mitigate and reduce the amount of time for both of the parties involved.

The server does not have to re-process the request. The client and server both save on bandwidth which costs money for both the client and server. Plenty of hosting companies charge for bandwidth and some ISPs too, especially on mobile devices. Let’s look at some code.

1
2
3
4
5
6
7
8
9
10
11
Route::group(['before' => 'cached.request', 'after' => 'cached.response'], function()
{
Route::get('/', function()
{
sleep(5);
return Response::json(array('test' => 'some data that takes a while to return'));
});
});

Route::filter('cached.request', 'CachingFilter@before');
Route::filter('cached.response', 'CachingFilter@after');

This code is sleeping for 5 seconds and then we could send a big chunk of data to the client. I’m just doing this sleep to show that on the next page refresh you will see results instantly because the server is responding with a 304. What does the CachingFilter look like? Let’s take a look.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class CachingFilter
{
/**
* Create the entity we will be searching etag for
*/
protected function entity($request)
{
return $request->url() . '-' . with(new DateTime)->format('d');
}

/**
* Should be run before routes are executed
* Will abort if etag match is found
*/
public function before($route, $request)
{
$entity = $this->entity($request);

$etag = new Etag($entity, $request->getEtags());

if ($etag->isValid())
{
App::abort(304);
}
}

/**
* Should be run after a route is executed
* Creates a new etag for this response content
*/
public function after($route, $request, $response)
{
$entity = $this->entity($request);

$etag = new Etag($entity, $request->getEtags());

$response->setEtag($etag->key());
}
}

Finally we will want to see what this Etag class is doing. It has two methods. One fetches the key for us. The other method will tell us if the etag is still valid or not.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Etag
{
public function __construct($entity, $etags)
{
$this->entity = $entity;
$this->etags = $etags;
}
/**
* Create an etag cached for given request and response.
*
* @return void
*/
public function key()
{
return md5($this->entity);
}

/**
* Does the browser have an Etag stored for this request?
*/
public function isValid()
{
$key = $this->key();

$etag = str_replace('"', '', $this->etags);

return $etag && $etag[0] == $key;
}
}

It’s important to note here that this only targets 1 client at a time. This type of client-side caching is useful for when the same url (resource) provides different content for different users. Imagine if you went to /users/me and saw someone else’s page?

However, in the case were you are fetching a specific set of entities then you might utilize database caching and Etags. The page /pages/how-i-met-your-mother is likely to be the same for all users of the system. Unfortunately this approach still requires the Laravel application to be booted so it will never be as fast as just serving a static html file.