Careful: `fresh_when last_modified: ...` without an object does not generate an E-Tag

To allow HTTP 304 responses Show archive.org snapshot , Rails offers the fresh_when Show archive.org snapshot method for controllers.

The most common way is to pass an ActiveRecord instance or scope, and fresh_when will set fitting E-Tag and Last-Modified headers for you. For scopes, an extra query is sent to the database.

fresh_when @users

If you do not want that magic to happen, e.g. because your scope is expensive to resolve, you can not use fresh_when like above.

You might come up with some way of knowing the timestamp, like keeping the latest update timestamp in a separate place that is inexpensive to read from, and now just want to pass that timestamp to fresh_when.

The problem

It might seem simple enough to just say:

updated_at = Rails.cache.fetch('updated_at_of_expensive_scope')
fresh_when last_modified: updated_at

The example above calls fresh_when without an object, so Rails will only set the Last-Modified header and not generate an E-Tag.
However, the Last-Modified header's resolution is using only seconds Show archive.org snapshot . If your timestamp holds milliseconds, they are discarded.

While that is fine for humans, automated tests may fail because of that: If two tests run within the same second frame, your application's response will have the same Last-Modified header value, even when both tests actually have different data.

While test browsers usually clear all data (cookies, LocalStorage, etc.), at least Chrome seems to keep its HTTP cache and your tests will fail "randomly" (= when they happen within a second).

How to fix it

You can probably start your test browser in incognito mode to fix that.

However, the root cause is that your application claims that two different responses are the same.
The proper solution is to pass your high resolution timestamp as the object to fresh_when and omit the last_modified option.

updated_at = Rails.cache.fetch('updated_at_of_expensive_scope')
fresh_when updated_at

Rails will generate an E-Tag header from that and all will be well.

Arne Hartherz Over 1 year ago