Skip to content

Commit

Permalink
Added ability to pass in :public => true to fresh_when, stale?, and e…
Browse files Browse the repository at this point in the history
…xpires_in to make the request proxy cachable [#2095 state:committed]

Signed-off-by: David Heinemeier Hansson <david@loudthinking.com>
  • Loading branch information
Gregg authored and dhh committed Feb 28, 2009
1 parent 7058c13 commit f2a32bd
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 7 deletions.
2 changes: 2 additions & 0 deletions actionpack/CHANGELOG
@@ -1,5 +1,7 @@
*2.3.1 [RC2] (February ?, 2009)* *2.3.1 [RC2] (February ?, 2009)*


* Added ability to pass in :public => true to fresh_when, stale?, and expires_in to make the request proxy cachable #2095 [Gregg Pollack]

* Fixed that passing a custom form builder would be forwarded to nested fields_for calls #2023 [Eloy Duran/Nate Wiger] * Fixed that passing a custom form builder would be forwarded to nested fields_for calls #2023 [Eloy Duran/Nate Wiger]


* Added partial scoping to TranslationHelper#translate, so if you call translate(".foo") from the people/index.html.erb template, you'll actually be calling I18n.translate("people.index.foo") [DHH] * Added partial scoping to TranslationHelper#translate, so if you call translate(".foo") from the people/index.html.erb template, you'll actually be calling I18n.translate("people.index.foo") [DHH]
Expand Down
42 changes: 35 additions & 7 deletions actionpack/lib/action_controller/base.rb
Expand Up @@ -1133,6 +1133,11 @@ def redirect_to_full_url(url, status)
# request is considered stale and should be generated from scratch. Otherwise, # request is considered stale and should be generated from scratch. Otherwise,
# it's fresh and we don't need to generate anything and a reply of "304 Not Modified" is sent. # it's fresh and we don't need to generate anything and a reply of "304 Not Modified" is sent.
# #
# Parameters:
# * <tt>:etag</tt>
# * <tt>:last_modified</tt>
# * <tt>:public</tt> By default the Cache-Control header is private, set this to true if you want your application to be cachable by other devices (proxy caches).
#
# Example: # Example:
# #
# def show # def show
Expand All @@ -1153,20 +1158,34 @@ def stale?(options)
# Sets the etag, last_modified, or both on the response and renders a # Sets the etag, last_modified, or both on the response and renders a
# "304 Not Modified" response if the request is already fresh. # "304 Not Modified" response if the request is already fresh.
# #
# Parameters:
# * <tt>:etag</tt>
# * <tt>:last_modified</tt>
# * <tt>:public</tt> By default the Cache-Control header is private, set this to true if you want your application to be cachable by other devices (proxy caches).
#
# Example: # Example:
# #
# def show # def show
# @article = Article.find(params[:id]) # @article = Article.find(params[:id])
# fresh_when(:etag => @article, :last_modified => @article.created_at.utc) # fresh_when(:etag => @article, :last_modified => @article.created_at.utc, :public => true)
# end # end
# #
# This will render the show template if the request isn't sending a matching etag or # This will render the show template if the request isn't sending a matching etag or
# If-Modified-Since header and just a "304 Not Modified" response if there's a match. # If-Modified-Since header and just a "304 Not Modified" response if there's a match.
#
def fresh_when(options) def fresh_when(options)
options.assert_valid_keys(:etag, :last_modified) options.assert_valid_keys(:etag, :last_modified, :public)


response.etag = options[:etag] if options[:etag] response.etag = options[:etag] if options[:etag]
response.last_modified = options[:last_modified] if options[:last_modified] response.last_modified = options[:last_modified] if options[:last_modified]

if options[:public]
cache_control = response.headers["Cache-Control"].split(",").map {|k| k.strip }
cache_control.delete("private")
cache_control.delete("no-cache")
cache_control << "public"
response.headers["Cache-Control"] = cache_control.join(', ')
end


if request.fresh?(response) if request.fresh?(response)
head :not_modified head :not_modified
Expand All @@ -1178,15 +1197,24 @@ def fresh_when(options)
# #
# Examples: # Examples:
# expires_in 20.minutes # expires_in 20.minutes
# expires_in 3.hours, :private => false # expires_in 3.hours, :public => true
# expires in 3.hours, 'max-stale' => 5.hours, :private => nil, :public => true # expires in 3.hours, 'max-stale' => 5.hours, :public => true
# #
# This method will overwrite an existing Cache-Control header. # This method will overwrite an existing Cache-Control header.
# See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities. # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities.
def expires_in(seconds, options = {}) #:doc: def expires_in(seconds, options = {}) #:doc:
cache_options = { 'max-age' => seconds, 'private' => true }.symbolize_keys.merge!(options.symbolize_keys) cache_control = response.headers["Cache-Control"].split(",").map {|k| k.strip }
cache_options.delete_if { |k,v| v.nil? or v == false }
cache_control = cache_options.map{ |k,v| v == true ? k.to_s : "#{k.to_s}=#{v.to_s}"} cache_control << "max-age=#{seconds}"
if options[:public]
cache_control.delete("private")
cache_control.delete("no-cache")
cache_control << "public"
end

# This allows for additional headers to be passed through like 'max-stale' => 5.hours
cache_control += options.symbolize_keys.reject{|k,v| k == :public || k == :private }.map{ |k,v| v == true ? k.to_s : "#{k.to_s}=#{v.to_s}"}

response.headers["Cache-Control"] = cache_control.join(', ') response.headers["Cache-Control"] = cache_control.join(', ')
end end


Expand Down
72 changes: 72 additions & 0 deletions actionpack/test/controller/render_test.rb
Expand Up @@ -36,6 +36,39 @@ def conditional_hello
render :action => 'hello_world' render :action => 'hello_world'
end end
end end

def conditional_hello_with_public_header
if stale?(:last_modified => Time.now.utc.beginning_of_day, :etag => [:foo, 123], :public => true)
render :action => 'hello_world'
end
end

def conditional_hello_with_public_header_and_expires_at
expires_in 1.minute
if stale?(:last_modified => Time.now.utc.beginning_of_day, :etag => [:foo, 123], :public => true)
render :action => 'hello_world'
end
end

def conditional_hello_with_expires_in
expires_in 1.minute
render :action => 'hello_world'
end

def conditional_hello_with_expires_in_with_public
expires_in 1.minute, :public => true
render :action => 'hello_world'
end

def conditional_hello_with_expires_in_with_public_with_more_keys
expires_in 1.minute, :public => true, 'max-stale' => 5.hours
render :action => 'hello_world'
end

def conditional_hello_with_expires_in_with_public_with_more_keys_old_syntax
expires_in 1.minute, :public => true, :private => nil, 'max-stale' => 5.hours
render :action => 'hello_world'
end


def conditional_hello_with_bangs def conditional_hello_with_bangs
render :action => 'hello_world' render :action => 'hello_world'
Expand Down Expand Up @@ -1464,6 +1497,35 @@ def test_render_call_to_partial_with_layout_in_main_layout_and_within_content_fo
end end
end end


class ExpiresInRenderTest < ActionController::TestCase
tests TestController

def setup
@request.host = "www.nextangle.com"
end

def test_expires_in_header
get :conditional_hello_with_expires_in
assert_equal "max-age=60, private", @response.headers["Cache-Control"]
end

def test_expires_in_header
get :conditional_hello_with_expires_in_with_public
assert_equal "max-age=60, public", @response.headers["Cache-Control"]
end

def test_expires_in_header_with_additional_headers
get :conditional_hello_with_expires_in_with_public_with_more_keys
assert_equal "max-age=60, public, max-stale=18000", @response.headers["Cache-Control"]
end

def test_expires_in_old_syntax
get :conditional_hello_with_expires_in_with_public_with_more_keys_old_syntax
assert_equal "max-age=60, public, max-stale=18000", @response.headers["Cache-Control"]
end
end


class EtagRenderTest < ActionController::TestCase class EtagRenderTest < ActionController::TestCase
tests TestController tests TestController


Expand Down Expand Up @@ -1553,6 +1615,16 @@ def test_etag_with_bang_should_obey_if_none_match
get :conditional_hello_with_bangs get :conditional_hello_with_bangs
assert_response :not_modified assert_response :not_modified
end end

def test_etag_with_public_true_should_set_header
get :conditional_hello_with_public_header
assert_equal "public", @response.headers['Cache-Control']
end

def test_etag_with_public_true_should_set_header_and_retain_other_headers
get :conditional_hello_with_public_header_and_expires_at
assert_equal "max-age=60, public", @response.headers['Cache-Control']
end


protected protected
def etag_for(text) def etag_for(text)
Expand Down

2 comments on commit f2a32bd

@spencermiles
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really terrible - changing :private to :public without any deprecation warning? Not cool.

@jeremy
Copy link
Member

@jeremy jeremy commented on f2a32bd Aug 4, 2009

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a mistake, Spencer. Please raise the issue on Lighthouse ticket #2095.

Please sign in to comment.