Question

In a small quest to improve caching in my Rails (3.2) application, I noticed that my Etag header changes for every single request.

I suppose this is because some part of my middleware is changing the response contents on every request, but I don't understand what. This is the result of running rake middleware:

use Rack::Cache
use ActionDispatch::Static
use Rack::Lock
use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007f462e790e98>
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::RemoteIp
use ActionDispatch::Callbacks
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ParamsParser
use ActionDispatch::Head
use Rack::ConditionalGet
use Rack::Deflater
use Rack::ETag
use ActionDispatch::BestStandardsSupport
use Warden::Manager
use Rack::Attack
use Rack::SslEnforcer
use HireFire::Middleware`

After some research I stumbled upon some information that Rack::Deflater is generating a timestamp for everything it deflates. I've tried moving Rack::Deflater after Rack::ETag (config.middleware.insert_after Rack::ETag, Rack::Deflater), but the ETag header still changes on every request.

Anyone here with more rack experience who could help me find out what is causing this behavior?

Was it helpful?

Solution

Here is a relevant source code of Rack::ETag:

def call(env)
  status, headers, body = @app.call(env)

  if etag_status?(status) && etag_body?(body) && !skip_caching?(headers)
    original_body = body
    digest, new_body = digest_body(body)
    body = Rack::BodyProxy.new(new_body) do
      original_body.close if original_body.respond_to?(:close)
    end
    headers['ETag'] = %("#{digest}") if digest
  end

  unless headers['Cache-Control']
    if digest
      headers['Cache-Control'] = @cache_control if @cache_control
    else
      headers['Cache-Control'] = @no_cache_control if @no_cache_control
    end
  end

  [status, headers, body]
end

As you can see, digest is calculated based on response body (using digest_body). Here's the source:

def digest_body(body)
  parts = []
  digest = nil

  body.each do |part|
    parts << part
    (digest ||= Digest::MD5.new) << part unless part.empty?
  end

  [digest && digest.hexdigest, parts]
end

You can hook inside of that method. I believe you will find that there's something dynamic in your response and this blocks you from using ETag header. For example, there maybe CSRF protection tokens in your forms, or something like that. I would strongly suggest you to use something like debugger or pry for placing breakpoints.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top