Back to Blog

Rails 5 switches from strong etags to weak etags

on March 8, 2016
This blog is part of our Rails 5 series.

ETag, short for entity tag, is a part of HTTP header and is used for web cache validation. ETag is a digest of the resource that uniquely identifies specific version of the resource. This helps browser and web servers determine if resource in the browser's cache is exactly same as the resource on the server.

Strong v/s Weak ETags

ETag supports strong and weak validation of the resource.

Strong ETag indicates that resource content is same for response body and the response headers.

Weak ETag indicates that the two representations are semantically equivalent. It compares only the response body.

Weak ETags are prefixed with W\ and thus one can easily distinguish between Weak ETags and Strong ETags.

1"543b39c23d8d34c232b457297d38ad99"    – Strong ETag
2W/"543b39c23d8d34c232b457297d38ad99"  – Weak ETag

W3 has an example page to illustrate how ETag matching works.

When server receives a request, it returns an ETag header as part of HTTP response. This ETag represents state of the resource. For the subsequent HTTP requests, client sends this ETag via If-None-Match header to identify if the resource is changed or not. The server will compare the current ETag and the one sent by the client. If ETag matches, server responds with 304 Not modified. This means resource content in the client's cache is up-to-date. If resource is changed, server will send updated resource along with the new ETag.

Let's see it in action.

ETags in Rails 4.x

Rails 4.x generates strong ETags by default i.e without W/ prefix.

1class ItemsController < ApplicationController
2  def show
3    @item = Item.find(params[:id])
4    fresh_when @item
5  end
6end

We are making first request to the server.

1$ curl -i http://localhost:3000/items/1
2
3HTTP/1.1 200 OK
4X-Frame-Options: SAMEORIGIN
5X-Xss-Protection: 1; mode=block
6X-Content-Type-Options: nosniff
7Etag: "618bbc92e2d35ea1945008b42799b0e7"
8Last-Modified: Sat, 30 Jan 2016 08:02:12 GMT
9Content-Type: text/html; charset=utf-8
10Cache-Control: max-age=0, private, must-revalidate
11X-Request-Id: 98359119-14ae-4e4e-8174-708abbc3fd4b
12X-Runtime: 0.412232
13Server: WEBrick/1.3.1 (Ruby/2.2.2/2015-04-13)
14Date: Fri, 04 Mar 2016 10:50:38 GMT
15Content-Length: 1014
16Connection: Keep-Alive

For the next request, we will send ETag that was sent by the sever. And notice that server returns 304 Not Modified.

1$ curl -i -H 'If-None-Match: "618bbc92e2d35ea1945008b42799b0e7"' http://localhost:3000/items/1
2
3HTTP/1.1 304 Not Modified
4X-Frame-Options: SAMEORIGIN
5X-Xss-Protection: 1; mode=block
6X-Content-Type-Options: nosniff
7Etag: "618bbc92e2d35ea1945008b42799b0e7"
8Last-Modified: Sat, 30 Jan 2016 08:02:12 GMT
9Cache-Control: max-age=0, private, must-revalidate
10X-Request-Id: e4447f82-b96c-4482-a5ff-4f5003910c18
11X-Runtime: 0.012878
12Server: WEBrick/1.3.1 (Ruby/2.2.2/2015-04-13)
13Date: Fri, 04 Mar 2016 10:51:22 GMT
14Connection: Keep-Alive

Rails 5 sets Weak ETags by default

In Rails 5, all ETags generated by Rails will be weak by default.

1$ curl -i http://localhost:3000/items/1
2
3HTTP/1.1 200 OK
4X-Frame-Options: SAMEORIGIN
5X-Xss-Protection: 1; mode=block
6X-Content-Type-Options: nosniff
7Etag: W/"b749c4dd1b20885128f9d9a1a8ba70b6"
8Last-Modified: Sat, 05 Mar 2016 00:00:00 GMT
9Content-Type: text/html; charset=utf-8
10Cache-Control: max-age=0, private, must-revalidate
11X-Request-Id: a24b986c-74f0-4e23-9b1d-0b52cb3ef906
12X-Runtime: 0.038372
13Server: WEBrick/1.3.1 (Ruby/2.2.3/2015-08-18)
14Date: Fri, 04 Mar 2016 10:48:35 GMT
15Content-Length: 1906
16Connection: Keep-Alive

Now for the second request, server will return 304 Not Modified response as before, but the ETag is weak ETag.

1$ curl -i -H 'If-None-Match: W/"b749c4dd1b20885128f9d9a1a8ba70b6"' http://localhost:3000/items/1
2
3HTTP/1.1 304 Not Modified
4X-Frame-Options: SAMEORIGIN
5X-Xss-Protection: 1; mode=block
6X-Content-Type-Options: nosniff
7Etag: W/"b749c4dd1b20885128f9d9a1a8ba70b6"
8Last-Modified: Sat, 05 Mar 2016 00:00:00 GMT
9Cache-Control: max-age=0, private, must-revalidate
10X-Request-Id: 7fc8a8b9-c7ff-4600-bf9b-c847201973cc
11X-Runtime: 0.005469
12Server: WEBrick/1.3.1 (Ruby/2.2.3/2015-08-18)
13Date: Fri, 04 Mar 2016 10:49:27 GMT
14Connection: Keep-Alive

Why this change?

Rails does not perform strong validation of ETags as implied by strong ETags spec. Rails just checks whether the incoming ETag from the request headers matches with the ETag of the generated response. It does not do byte by byte comparison of the response.

This was true even before Rails 5. So this change is more of a course correction. Rack also generates weak ETags by default because of similar reasons.

Mark Notthingham is chair of HTTP Working Group and he has written about etags which has some useful links to other ETag resources.

How to use strong ETags in Rails 5

If we want to bypass default Rails 5 behavior to use strong ETags then we can do by following way.

1
2class ItemsController < ApplicationController
3  def show
4    @item = Item.find(params[:id])
5    fresh_when strong_etag: @item
6  end
7end
8

This will generate strong Etag i.e without W/ prefix.


You might also like

If you liked this blog post, check out similar ones from BigBinary