Skip to content

Commit 0b4e8c8

Browse files
committed
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.
1 parent 7fe0f27 commit 0b4e8c8

File tree

2 files changed

+44
-10
lines changed

2 files changed

+44
-10
lines changed

actionpack/lib/action_dispatch/http/cache.rb

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -84,17 +84,29 @@ def etag=(etag)
8484
LAST_MODIFIED = "Last-Modified".freeze
8585
ETAG = "ETag".freeze
8686
CACHE_CONTROL = "Cache-Control".freeze
87+
SPESHUL_KEYS = %w[extras no-cache max-age public must-revalidate]
88+
89+
def cache_control_headers
90+
cache_control = {}
91+
if cc = self[CACHE_CONTROL]
92+
cc.delete(' ').split(',').each do |segment|
93+
directive, argument = segment.split('=', 2)
94+
case directive
95+
when *SPESHUL_KEYS
96+
key = directive.tr('-', '_')
97+
cache_control[key.to_sym] = argument || true
98+
else
99+
cache_control[:extras] ||= []
100+
cache_control[:extras] << segment
101+
end
102+
end
103+
end
104+
cache_control
105+
end
87106

88107
def prepare_cache_control!
89-
@cache_control = {}
108+
@cache_control = cache_control_headers
90109
@etag = self[ETAG]
91-
92-
if cache_control = self[CACHE_CONTROL]
93-
cache_control.split(/,\s*/).each do |segment|
94-
first, last = segment.split("=")
95-
@cache_control[first.to_sym] = last || true
96-
end
97-
end
98110
end
99111

100112
def handle_conditional_get!
@@ -110,14 +122,24 @@ def handle_conditional_get!
110122
MUST_REVALIDATE = "must-revalidate".freeze
111123

112124
def set_conditional_cache_control!
113-
return if self[CACHE_CONTROL].present?
125+
control = {}
126+
cc_headers = cache_control_headers
127+
if extras = cc_headers.delete(:extras)
128+
@cache_control[:extras] ||= []
129+
@cache_control[:extras] += extras
130+
@cache_control[:extras].uniq!
131+
end
114132

115-
control = @cache_control
133+
control.merge! cc_headers
134+
control.merge! @cache_control
116135

117136
if control.empty?
118137
headers[CACHE_CONTROL] = DEFAULT_CACHE_CONTROL
119138
elsif control[:no_cache]
120139
headers[CACHE_CONTROL] = NO_CACHE
140+
if control[:extras]
141+
headers[CACHE_CONTROL] += ", #{control[:extras].join(', ')}"
142+
end
121143
else
122144
extras = control[:extras]
123145
max_age = control[:max_age]

actionpack/test/controller/render_test.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@ def conditional_hello_with_expires_now
116116
render :action => 'hello_world'
117117
end
118118

119+
def conditional_hello_with_cache_control_headers
120+
response.headers['Cache-Control'] = 'no-transform'
121+
expires_now
122+
render :action => 'hello_world'
123+
end
124+
119125
def conditional_hello_with_bangs
120126
render :action => 'hello_world'
121127
end
@@ -1512,6 +1518,12 @@ def test_expires_now
15121518
assert_equal "no-cache", @response.headers["Cache-Control"]
15131519
end
15141520

1521+
def test_expires_now
1522+
get :conditional_hello_with_cache_control_headers
1523+
assert_match /no-cache/, @response.headers["Cache-Control"]
1524+
assert_match /no-transform/, @response.headers["Cache-Control"]
1525+
end
1526+
15151527
def test_date_header_when_expires_in
15161528
time = Time.mktime(2011,10,30)
15171529
Time.stubs(:now).returns(time)

0 commit comments

Comments
 (0)