From 7d23b5914b0772bf295f78543732f24c315ce065 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Sat, 19 Aug 2023 18:01:41 +0100 Subject: [PATCH 1/2] Allow customisation of transferable headers Headers specified in the :transfer_headers option are passed (transferred) from responses from the backend through to responses sent to the client (included cached entries). --- lib/rack/cache/context.rb | 2 +- lib/rack/cache/options.rb | 8 ++++++++ test/context_test.rb | 24 ++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/lib/rack/cache/context.rb b/lib/rack/cache/context.rb index 58996b7..583f4bf 100644 --- a/lib/rack/cache/context.rb +++ b/lib/rack/cache/context.rb @@ -237,7 +237,7 @@ def validate(entry) entry = entry.dup entry.headers.delete('date') - %w[Date expires cache-control etag last-modified].each do |name| + %w[Date expires cache-control etag last-modified].concat(transfer_headers).each do |name| next unless value = response.headers[name] entry.headers[name] = value end diff --git a/lib/rack/cache/options.rb b/lib/rack/cache/options.rb index 0007605..d2d5112 100644 --- a/lib/rack/cache/options.rb +++ b/lib/rack/cache/options.rb @@ -86,6 +86,13 @@ def option_name(key) # Default: ['set-cookie'] option_accessor :ignore_headers + # Set of response headers that are transferred from the backend response + # onto the cache entry when validating a cached entity (after receiving a + # 304 response from the backend) before sending it to the client. + # + # Default: [] + option_accessor :transfer_headers + # Set of request headers that trigger "private" cache-control behavior # on responses that don't explicitly state whether the response is # public or private via a cache-control directive. Applications that use @@ -149,6 +156,7 @@ def initialize_options(options={}) 'rack-cache.entitystore' => 'heap:/', 'rack-cache.default_ttl' => 0, 'rack-cache.ignore_headers' => ['set-cookie'], + 'rack-cache.transfer_headers' => [], 'rack-cache.private_headers' => ['Authorization', 'Cookie'], 'rack-cache.allow_reload' => false, 'rack-cache.allow_revalidate' => false, diff --git a/test/context_test.rb b/test/context_test.rb index acf570a..15470d9 100644 --- a/test/context_test.rb +++ b/test/context_test.rb @@ -776,6 +776,30 @@ cache.trace.wont_include :miss end + it 'includes specified additional headers from the backend in responses to the client' do + count = 0 + respond_with do |req,res| + count += 1 + res['ETAG'] = '"12345"' + res['unique-snowflake'] = '"so-special"' + res.status = (count == 1) ? 200 : 304 + end + + get '/', 'rack-cache.transfer_headers' => ['unique-snowflake'] + assert app.called? + assert response.ok? + response.headers.must_include 'unique-snowflake' + response['unique-snowflake'].must_equal '"so-special"' + cache.trace.must_include :miss + + get '/', 'rack-cache.transfer_headers' => ['unique-snowflake'] + assert app.called? + assert response.ok? + response.headers.must_include 'unique-snowflake' + response['unique-snowflake'].must_equal '"so-special"' + cache.trace.must_include :valid + end + it 'replaces cached responses when validation results in non-304 response' do timestamp = Time.now.httpdate count = 0 From 816a92bfeb0c57daeadbe0884688295befdcebbb Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Sat, 19 Aug 2023 21:41:04 +0100 Subject: [PATCH 2/2] Allow customisation of non-cached headers Headers specified in the :non_cached_headers option are included in responses but not stored in cached entities. --- lib/rack/cache/context.rb | 2 +- lib/rack/cache/meta_store.rb | 3 ++- lib/rack/cache/options.rb | 33 ++++++++++++++++++++------------- test/context_test.rb | 28 ++++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 15 deletions(-) diff --git a/lib/rack/cache/context.rb b/lib/rack/cache/context.rb index 583f4bf..bf81950 100644 --- a/lib/rack/cache/context.rb +++ b/lib/rack/cache/context.rb @@ -286,7 +286,7 @@ def fetch # Write the response to the cache. def store(response) strip_ignore_headers(response) - metastore.store(@request, response, entitystore) + metastore.store(@request, response, entitystore, non_cached_headers) response.headers['age'] = response.age.to_s rescue => e log_error(e) diff --git a/lib/rack/cache/meta_store.rb b/lib/rack/cache/meta_store.rb index 7fc63a9..6637c65 100644 --- a/lib/rack/cache/meta_store.rb +++ b/lib/rack/cache/meta_store.rb @@ -58,7 +58,7 @@ def lookup(request, entity_store) # Write a cache entry to the store under the given key. Existing # entries are read and any that match the response are removed. # This method calls #write with the new list of cache entries. - def store(request, response, entity_store) + def store(request, response, entity_store, non_cached_headers = []) key = cache_key(request) stored_env = persist_request(request) @@ -97,6 +97,7 @@ def store(request, response, entity_store) headers = persist_response(response) headers.delete('age') + non_cached_headers.each{ |h| headers.delete(h) } entries.unshift [stored_env, headers] if request.env['rack-cache.use_native_ttl'] && response.fresh? diff --git a/lib/rack/cache/options.rb b/lib/rack/cache/options.rb index d2d5112..b6680a8 100644 --- a/lib/rack/cache/options.rb +++ b/lib/rack/cache/options.rb @@ -93,6 +93,12 @@ def option_name(key) # Default: [] option_accessor :transfer_headers + # Set of response headers that will be passed to the client when present + # in any responses but will not be stored in cached entries. + # + # Default: [] + option_accessor :non_cached_headers + # Set of request headers that trigger "private" cache-control behavior # on responses that don't explicitly state whether the response is # public or private via a cache-control directive. Applications that use @@ -149,19 +155,20 @@ def set(option, value=self, &block) private def initialize_options(options={}) @default_options = { - 'rack-cache.cache_key' => Key, - 'rack-cache.verbose' => true, - 'rack-cache.storage' => Rack::Cache::Storage.instance, - 'rack-cache.metastore' => 'heap:/', - 'rack-cache.entitystore' => 'heap:/', - 'rack-cache.default_ttl' => 0, - 'rack-cache.ignore_headers' => ['set-cookie'], - 'rack-cache.transfer_headers' => [], - 'rack-cache.private_headers' => ['Authorization', 'Cookie'], - 'rack-cache.allow_reload' => false, - 'rack-cache.allow_revalidate' => false, - 'rack-cache.use_native_ttl' => false, - 'rack-cache.fault_tolerant' => false, + 'rack-cache.cache_key' => Key, + 'rack-cache.verbose' => true, + 'rack-cache.storage' => Rack::Cache::Storage.instance, + 'rack-cache.metastore' => 'heap:/', + 'rack-cache.entitystore' => 'heap:/', + 'rack-cache.default_ttl' => 0, + 'rack-cache.ignore_headers' => ['set-cookie'], + 'rack-cache.transfer_headers' => [], + 'rack-cache.non_cached_headers' => [], + 'rack-cache.private_headers' => ['Authorization', 'Cookie'], + 'rack-cache.allow_reload' => false, + 'rack-cache.allow_revalidate' => false, + 'rack-cache.use_native_ttl' => false, + 'rack-cache.fault_tolerant' => false, } self.options = options end diff --git a/test/context_test.rb b/test/context_test.rb index 15470d9..1444055 100644 --- a/test/context_test.rb +++ b/test/context_test.rb @@ -800,6 +800,34 @@ cache.trace.must_include :valid end + it 'excludes specified headers from being cached in responses' do + count = 0 + respond_with do |req,res| + count += 1 + res['ETAG'] = '"12345"' + if count == 1 + res['not-cached'] = '"1"' + end + res.status = (count == 1) ? 200 : 304 + end + + get '/', 'rack-cache.non_cached_headers' => ['not-cached'] + assert app.called? + assert response.ok? + response.headers.must_include 'not-cached' + cache.trace.must_include :miss + cache.trace.must_include :store + cache.trace.wont_include :ignore + + get '/', 'rack-cache.non_cached_headers' => ['not-cached'] + assert app.called? + assert response.ok? + response.headers.wont_include 'not-cached' + cache.trace.must_include :stale + cache.trace.must_include :valid + cache.trace.must_include :store + end + it 'replaces cached responses when validation results in non-304 response' do timestamp = Time.now.httpdate count = 0