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.
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.
I took the code from @grosser's answer and turned it into a Gem:
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.
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
It looks like Rack::ETag will use weak-etags in the future:
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.