Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Ensure that cache-control headers are merged

There are several aspects to this commit, that don't well fit into broken down
commits, so they are detailed here:

 * When a user uses response.headers['Cache-Control'] = some_value, then the
   documented convention in ConditionalGet is not adhered to, in this case,
   response.cache_control is ignored due to `return if
   self[CACHE_CONTROL].present?`
 * When a middleware sets cache-control headers that would clobber, they're
   converted to symbols directly, without underscores. This would lead to bugs.
 * Items that would live in :extras if set through expires_in, are placed
   directly in the @cache_control hash, and not respected in many cases
   (somewhat adhering to the aforementioned documentation).
 * Although quite useless, any directive named 'extras' would be ignored.

The general convention applied is that expires_* take precedence, but no longer
overwrite everything and expires_* are ALWAYS applied, even if the header is
set.

I am still unhappy about the contents of this commit, and the code in general.
Ideally it should be refactored to no longer use :extras. I'd likely recommend
expanding @cache_control into a class, and giving it the power to handle the
merge in a more efficient fashion. Such a commit would be a larger change that
could have additional semantic changes for other libraries unless they utilize
expires_in in very standard ways.
  • Loading branch information...
commit 0b4e8c8d84fe4f66e85cf7cfeebe604e0f2849db 1 parent 7fe0f27
James Tucker raggi authored
42 actionpack/lib/action_dispatch/http/cache.rb
View
@@ -84,17 +84,29 @@ def etag=(etag)
LAST_MODIFIED = "Last-Modified".freeze
ETAG = "ETag".freeze
CACHE_CONTROL = "Cache-Control".freeze
+ SPESHUL_KEYS = %w[extras no-cache max-age public must-revalidate]
+
+ def cache_control_headers
+ cache_control = {}
+ if cc = self[CACHE_CONTROL]
+ cc.delete(' ').split(',').each do |segment|
+ directive, argument = segment.split('=', 2)
+ case directive
+ when *SPESHUL_KEYS
+ key = directive.tr('-', '_')
+ cache_control[key.to_sym] = argument || true
+ else
+ cache_control[:extras] ||= []
+ cache_control[:extras] << segment
+ end
+ end
+ end
+ cache_control
+ end
def prepare_cache_control!
- @cache_control = {}
+ @cache_control = cache_control_headers
@etag = self[ETAG]
-
- if cache_control = self[CACHE_CONTROL]
- cache_control.split(/,\s*/).each do |segment|
- first, last = segment.split("=")
- @cache_control[first.to_sym] = last || true
- end
- end
end
def handle_conditional_get!
@@ -110,14 +122,24 @@ def handle_conditional_get!
MUST_REVALIDATE = "must-revalidate".freeze
def set_conditional_cache_control!
- return if self[CACHE_CONTROL].present?
+ control = {}
+ cc_headers = cache_control_headers
+ if extras = cc_headers.delete(:extras)
+ @cache_control[:extras] ||= []
+ @cache_control[:extras] += extras
+ @cache_control[:extras].uniq!
+ end
- control = @cache_control
+ control.merge! cc_headers
+ control.merge! @cache_control
if control.empty?
headers[CACHE_CONTROL] = DEFAULT_CACHE_CONTROL
elsif control[:no_cache]
headers[CACHE_CONTROL] = NO_CACHE
+ if control[:extras]
+ headers[CACHE_CONTROL] += ", #{control[:extras].join(', ')}"
+ end
else
extras = control[:extras]
max_age = control[:max_age]
12 actionpack/test/controller/render_test.rb
View
@@ -116,6 +116,12 @@ def conditional_hello_with_expires_now
render :action => 'hello_world'
end
+ def conditional_hello_with_cache_control_headers
+ response.headers['Cache-Control'] = 'no-transform'
+ expires_now
+ render :action => 'hello_world'
+ end
+
def conditional_hello_with_bangs
render :action => 'hello_world'
end
@@ -1512,6 +1518,12 @@ def test_expires_now
assert_equal "no-cache", @response.headers["Cache-Control"]
end
+ def test_expires_now
+ get :conditional_hello_with_cache_control_headers
+ assert_match /no-cache/, @response.headers["Cache-Control"]
+ assert_match /no-transform/, @response.headers["Cache-Control"]
+ end
+
def test_date_header_when_expires_in
time = Time.mktime(2011,10,30)
Time.stubs(:now).returns(time)
Please sign in to comment.
Something went wrong with that request. Please try again.