Weak ETAGs in Rails?

2019-01-24 11:18发布

问题:

What is the best way to tell rails to use weak instead of strong ETAGs when using methods fresh_when and stale??

The reason I ask is that nginx (correctly) removes strong ETAG headers from responses when on-the-fly gzipping is enabled.

回答1:

I took the code from @grosser's answer and turned it into a Gem:

  • https://rubygems.org/gems/rails_weak_etags
  • https://github.com/johnnaegle/rails_weak_etags

You can just add this to your gemfile:

gem 'rails_weak_etags'

And it will be installed into your middleware before Rack::ConditionalGet:

> bundle exec rake middleware
....
use RailsWeakEtags::Middleware
use Rack::ConditionalGet
use Rack::ETag
....

Then all the e-tags generated by rails, either with Rack::ETag or with explicit e-tags will be converted to weak. Using a patched, or version > 1.7.3 of nginx, will then let you use e-tags and gzip compression.

RACK 1.6 defaults etags to weak - this gem is no longer helpful if you upgrade.



回答2:

middleware:

class WeakEtagMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    # make request etags "strong"
    etag = env['HTTP_IF_NONE_MATCH']
    if etag && etag =~ /^W\//
      env['HTTP_IF_NONE_MATCH'] = etag[2..-1]
    end

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

    # make response etags "weak"
    etag = headers['ETag']
    if etag && etag !~ /^W\//
      headers['ETag'] = "W/#{etag}"
    end

    [status, headers, body]
  end
end

plus add middleware

Rails.application.config.middleware.insert_before(Rack::ETag, WeakEtagMiddleware)

plus unit tests

context WeakEtagMiddleware do
  let(:backend) { Rack::ConditionalGet.new(Rack::ETag.new(lambda { |env| [env["status"] || 200, {}, ["XXX"]] })) }
  let(:app) { WeakEtagMiddleware.new(backend) }
  let(:expected_digest_1) { "bc9189406be84ec297464a514221406d" }
  let(:env) { {"REQUEST_METHOD" => "GET"} }

  should "converts etags to weak" do
    status, headers, body = app.call(env)
    assert_equal %{W/"#{expected_digest_1}"}, headers["ETag"]
    assert_equal status, 200
  end

  should "not add etags to responses without etag" do
    status, headers, body = app.call(env.merge("status" => 400))
    refute headers["ETag"]
    assert_equal status, 400
  end

  should "recognize weak ETags" do
    status, headers, body = app.call(env.merge("HTTP_IF_NONE_MATCH" => %{W/"#{expected_digest_1}"}))
    assert_equal status, 304
  end

  should "not recognize invalid ETags" do
    status, headers, body = app.call(env.merge("HTTP_IF_NONE_MATCH" => %{W/"something-not-fresh"}))
    assert_equal status, 200
  end
end

plus integration tests

require_relative "../helpers/test_helper"

class WeakEtagsTest < ActionController::IntegrationTest
  class TestController < ActionController::Base
    def auto
      render :text => "XXX"
    end

    def fresh
      if stale? :etag => "YYY"
        render :text => "XXX"
      end
    end
  end

  additional_routes do
    get '/test/weak_etags/:action', :controller => 'weak_etags_test/test'
  end

  fixtures :accounts, :users

  context "weak etags" do
    let(:expected_digest_1) { "bc9189406be84ec297464a514221406d" }
    let(:expected_digest_2) { "fd7c5c4fdaa97163ee4ba8842baa537a" }

    should "auto adds weak etags" do
      get "/test/weak_etags/auto"
      assert_equal "XXX", @response.body
      assert_equal %{W/"#{expected_digest_1}"}, @response.headers["ETag"]
    end

    should "adds weak etags through fresh_when" do
      get "/test/weak_etags/fresh"
      assert_equal "XXX", @response.body
      assert_equal %{W/"#{expected_digest_2}"}, @response.headers["ETag"]
    end

    should "recognize auto-added ETags" do
      get "/test/weak_etags/auto", {}, {"HTTP_IF_NONE_MATCH" => %{W/"#{expected_digest_1}"}}
      assert_response :not_modified
    end

    should "recognize fresh ETags" do
      get "/test/weak_etags/fresh", {}, {"HTTP_IF_NONE_MATCH" => %{W/"#{expected_digest_2}"}}
      assert_response :not_modified
    end
  end
end


回答3:

It looks like Rack::ETag will use weak-etags in the future:

  • https://github.com/rack/rack/issues/681 https://github.com/rack/rack/commit/12528d4567d8e6c1c7e9422fee6cd8b43c4389bf


回答4:

Here's an alternative that avoids making any changes in your application server. This directive converts all etags returned by your application to weak etags before they get stripped from the response. Put it inside your inside your nginx config:

location / {
  add_header ETag "W/$sent_http_ETAG";
}

I've checked that this works with nginx 1.7.6.