1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

etag! and last_modified! conditional GET helpers

This commit is contained in:
Jeremy Kemper 2008-07-16 04:32:15 -07:00
parent a1fcbd971d
commit 57a2780f14
4 changed files with 151 additions and 62 deletions

View file

@ -1,5 +1,9 @@
*Edge*
* Conditional GET utility methods. [Jeremy Kemper]
* etag!([:admin, post, current_user]) sets the ETag response header and returns head(:not_modified) if it matches the If-None-Match request header.
* last_modified!(post.updated_at) sets Last-Modified and returns head(:not_modified) if it's no later than If-Modified-Since.
* All 2xx requests are considered successful [Josh Peek]
* Fixed that AssetTagHelper#compute_public_path shouldn't cache the asset_host along with the source or per-request proc's won't run [DHH]

View file

@ -519,6 +519,8 @@ module ActionController #:nodoc:
public
# Extracts the action_name from the request parameters and performs that action.
def process(request, response, method = :perform_action, *arguments) #:nodoc:
response.request = request
initialize_template_class(response)
assign_shortcuts(request, response)
initialize_current_url
@ -529,8 +531,6 @@ module ActionController #:nodoc:
send(method, *arguments)
assign_default_content_type_and_charset
response.request = request
response.prepare! unless component_request?
response
ensure
@ -968,6 +968,17 @@ module ActionController #:nodoc:
render :nothing => true, :status => status
end
# Sets the Last-Modified response header. Returns 304 Not Modified if the
# If-Modified-Since request header is <= last modified.
def last_modified!(utc_time)
head(:not_modified) if response.last_modified!(utc_time)
end
# Sets the ETag response header. Returns 304 Not Modified if the
# If-None-Match request header matches.
def etag!(etag)
head(:not_modified) if response.etag!(etag)
end
# Clears the rendered results, allowing for another render to be performed.
def erase_render_results #:nodoc:

View file

@ -41,20 +41,48 @@ module ActionController
set_content_length!
end
# Sets the Last-Modified response header. Returns whether it's older than
# the If-Modified-Since request header.
def last_modified!(utc_time)
headers['Last-Modified'] ||= utc_time.httpdate
if request && since = request.headers['HTTP_IF_MODIFIED_SINCE']
utc_time <= Time.rfc2822(since)
end
end
# Sets the ETag response header. Returns whether it matches the
# If-None-Match request header.
def etag!(tag)
headers['ETag'] ||= %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(tag))}")
if request && request.headers['HTTP_IF_NONE_MATCH'] == headers['ETag']
true
end
end
private
def handle_conditional_get!
if body.is_a?(String) && (headers['Status'] ? headers['Status'][0..2] == '200' : true) && !body.empty?
self.headers['ETag'] ||= %("#{Digest::MD5.hexdigest(body)}")
self.headers['Cache-Control'] = 'private, max-age=0, must-revalidate' if headers['Cache-Control'] == DEFAULT_HEADERS['Cache-Control']
if nonempty_ok_response?
set_conditional_cache_control!
if request.headers['HTTP_IF_NONE_MATCH'] == headers['ETag']
self.headers['Status'] = '304 Not Modified'
if etag!(body)
headers['Status'] = '304 Not Modified'
self.body = ''
end
end
end
def nonempty_ok_response?
status = headers['Status']
ok = !status || status[0..2] == '200'
ok && body.is_a?(String) && !body.empty?
end
def set_conditional_cache_control!
if headers['Cache-Control'] == DEFAULT_HEADERS['Cache-Control']
headers['Cache-Control'] = 'private, max-age=0, must-revalidate'
end
end
def convert_content_type!
if content_type = headers.delete("Content-Type")
self.headers["type"] = content_type
@ -73,4 +101,4 @@ module ActionController
self.headers["Content-Length"] = body.size unless body.respond_to?(:call)
end
end
end
end

View file

@ -8,14 +8,18 @@ module Fun
end
end
# FIXME: crashes Ruby 1.9
class TestController < ActionController::Base
layout :determine_layout
def hello_world
end
def conditional_hello
etag! [:foo, 123]
last_modified! Time.now.utc.beginning_of_day
render :action => 'hello_world' unless performed?
end
def render_hello_world
render :template => "test/hello_world"
end
@ -408,58 +412,6 @@ class RenderTest < Test::Unit::TestCase
assert_equal "Goodbye, Local David", @response.body
end
def test_render_200_should_set_etag
get :render_hello_world_from_variable
assert_equal etag_for("hello david"), @response.headers['ETag']
assert_equal "private, max-age=0, must-revalidate", @response.headers['Cache-Control']
end
def test_render_against_etag_request_should_304_when_match
@request.headers["HTTP_IF_NONE_MATCH"] = etag_for("hello david")
get :render_hello_world_from_variable
assert_equal "304 Not Modified", @response.headers['Status']
assert @response.body.empty?
end
def test_render_against_etag_request_should_200_when_no_match
@request.headers["HTTP_IF_NONE_MATCH"] = etag_for("hello somewhere else")
get :render_hello_world_from_variable
assert_equal "200 OK", @response.headers['Status']
assert !@response.body.empty?
end
def test_render_with_etag
get :render_hello_world_from_variable
expected_etag = etag_for('hello david')
assert_equal expected_etag, @response.headers['ETag']
@request.headers["HTTP_IF_NONE_MATCH"] = expected_etag
get :render_hello_world_from_variable
assert_equal "304 Not Modified", @response.headers['Status']
@request.headers["HTTP_IF_NONE_MATCH"] = "\"diftag\""
get :render_hello_world_from_variable
assert_equal "200 OK", @response.headers['Status']
end
def render_with_404_shouldnt_have_etag
get :render_custom_code
assert_nil @response.headers['ETag']
end
def test_etag_should_not_be_changed_when_already_set
expected_etag = etag_for("hello somewhere else")
@response.headers["ETag"] = expected_etag
get :render_hello_world_from_variable
assert_equal expected_etag, @response.headers['ETag']
end
def test_etag_should_govern_renders_with_layouts_too
get :builder_layout_test
assert_equal "<wrapper>\n<html>\n <p>Hello </p>\n<p>This is grand!</p>\n</html>\n</wrapper>\n", @response.body
assert_equal etag_for("<wrapper>\n<html>\n <p>Hello </p>\n<p>This is grand!</p>\n</html>\n</wrapper>\n"), @response.headers['ETag']
end
def test_should_render_formatted_template
get :formatted_html_erb
assert_equal 'formatted html erb', @response.body
@ -515,9 +467,103 @@ class RenderTest < Test::Unit::TestCase
get :render_xml_with_custom_content_type
assert_equal "application/atomsvc+xml", @response.content_type
end
end
class EtagRenderTest < Test::Unit::TestCase
def setup
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
@controller = TestController.new
@request.host = "www.nextangle.com"
end
def test_render_200_should_set_etag
get :render_hello_world_from_variable
assert_equal etag_for("hello david"), @response.headers['ETag']
assert_equal "private, max-age=0, must-revalidate", @response.headers['Cache-Control']
end
def test_render_against_etag_request_should_304_when_match
@request.headers["HTTP_IF_NONE_MATCH"] = etag_for("hello david")
get :render_hello_world_from_variable
assert_equal "304 Not Modified", @response.headers['Status']
assert @response.body.empty?
end
def test_render_against_etag_request_should_200_when_no_match
@request.headers["HTTP_IF_NONE_MATCH"] = etag_for("hello somewhere else")
get :render_hello_world_from_variable
assert_equal "200 OK", @response.headers['Status']
assert !@response.body.empty?
end
def test_render_with_etag
get :render_hello_world_from_variable
expected_etag = etag_for('hello david')
assert_equal expected_etag, @response.headers['ETag']
@request.headers["HTTP_IF_NONE_MATCH"] = expected_etag
get :render_hello_world_from_variable
assert_equal "304 Not Modified", @response.headers['Status']
@request.headers["HTTP_IF_NONE_MATCH"] = "\"diftag\""
get :render_hello_world_from_variable
assert_equal "200 OK", @response.headers['Status']
end
def render_with_404_shouldnt_have_etag
get :render_custom_code
assert_nil @response.headers['ETag']
end
def test_etag_should_not_be_changed_when_already_set
expected_etag = etag_for("hello somewhere else")
@response.headers["ETag"] = expected_etag
get :render_hello_world_from_variable
assert_equal expected_etag, @response.headers['ETag']
end
def test_etag_should_govern_renders_with_layouts_too
get :builder_layout_test
assert_equal "<wrapper>\n<html>\n <p>Hello </p>\n<p>This is grand!</p>\n</html>\n</wrapper>\n", @response.body
assert_equal etag_for("<wrapper>\n<html>\n <p>Hello </p>\n<p>This is grand!</p>\n</html>\n</wrapper>\n"), @response.headers['ETag']
end
protected
def etag_for(text)
%("#{Digest::MD5.hexdigest(text)}")
end
end
class LastModifiedRenderTest < Test::Unit::TestCase
def setup
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
@controller = TestController.new
@request.host = "www.nextangle.com"
@last_modified = Time.now.utc.beginning_of_day.httpdate
end
def test_responds_with_last_modified
get :conditional_hello
assert_equal @last_modified, @response.headers['Last-Modified']
end
def test_request_not_modified
@request.headers["HTTP_IF_MODIFIED_SINCE"] = @last_modified
get :conditional_hello
assert_equal "304 Not Modified", @response.headers['Status']
assert @response.body.blank?, @response.body
assert_equal @last_modified, @response.headers['Last-Modified']
end
def test_request_modified
@request.headers["HTTP_IF_MODIFIED_SINCE"] = 'Thu, 16 Jul 2008 00:00:00 GMT'
get :conditional_hello
assert_equal "200 OK", @response.headers['Status']
assert !@response.body.blank?
assert_equal @last_modified, @response.headers['Last-Modified']
end
end