Read more

Careful when using Time objects for generating ETags

Arne Hartherz
September 30, 2022Software engineer at makandra GmbH

You can use ETags to allow clients to use cached responses, if your application would send the same contents as before.

Illustration online protection

Rails Long Term Support

Rails LTS provides security patches for old versions of Ruby on Rails (2.3, 3.2, 4.2 and 5.2)

  • Prevents you from data breaches and liability risks
  • Upgrade at your own pace
  • Works with modern Rubies
Read more Show archive.org snapshot

Besides what "actually" defines your response's contents, your application probably also considers "global" conditions, like which user is signed in:

class ApplicationController < ActionController::Base

  etag { current_user&.id }
  etag { current_user&.updated_at }

end

Under the hood, Rails generates an ETag header value like W/"f14ce3710a2a3187802cadc7e0c8ea99". In doing so, all objects from that etaggers list are serialized and Time objects lose their millisecond resolution.

You likely won't notice for a while (ask me how I know) but occasionally encounter tests which are "randomly failing".

Example

When users change their name and visit a page they visited less than a second ago (yes, that is not an issue with humans), your application will respond with an HTTP 304 and the browser will re-use the response from before which still contained the old username.

To avoid that, convert any Time objects into a string or number before an ETag is generated from them.

class ApplicationController < ActionController::Base

  etag { current_user&.id }
  etag { current_user&.updated_at&.to_f }

end

You might even want to patch Rails' etagging logic so you don't have to remember doing that. Here you go:

class ApplicationController < ActionController::Base
  
  etag { current_user&.id }
  etag { current_user&.updated_at }
  
  private
  
  def combine_etags(...)
    # When using Time objects for etagging, their milliseconds are ignored because of how
    # ActiveSupport::Cache#expand_cache_key works (which is called to build ETag contents).
    # We convert them to floats to avoid that.
    super.map! { |object| object.is_a?(Time) ? object.to_f : object }
  end

end
Posted by Arne Hartherz to makandra dev (2022-09-30 17:58)