Skip to content

Commit

Permalink
etag! and last_modified! conditional GET helpers
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremy committed Jul 18, 2008
1 parent a1fcbd9 commit 57a2780
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 61 deletions.
4 changes: 4 additions & 0 deletions actionpack/CHANGELOG
Original file line number Diff line number Diff line change
@@ -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]
Expand Down
15 changes: 13 additions & 2 deletions actionpack/lib/action_controller/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,8 @@ def exempt_from_layout(*extensions)
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
Expand All @@ -529,8 +531,6 @@ def process(request, response, method = :perform_action, *arguments) #:nodoc:
send(method, *arguments)

assign_default_content_type_and_charset

response.request = request
response.prepare! unless component_request?
response
ensure
Expand Down Expand Up @@ -968,6 +968,17 @@ def head(*args)
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:
Expand Down
40 changes: 34 additions & 6 deletions actionpack/lib/action_controller/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,48 @@ def prepare!
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
Expand All @@ -73,4 +101,4 @@ def set_content_length!
self.headers["Content-Length"] = body.size unless body.respond_to?(:call)
end
end
end
end
152 changes: 99 additions & 53 deletions actionpack/test/controller/render_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,18 @@ def hello_world
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
Expand Down Expand Up @@ -408,6 +412,72 @@ def test_accessing_local_assigns_in_inline_template
assert_equal "Goodbye, Local David", @response.body
end

def test_should_render_formatted_template
get :formatted_html_erb
assert_equal 'formatted html erb', @response.body
end

def test_should_render_formatted_xml_erb_template
get :formatted_xml_erb, :format => :xml
assert_equal '<test>passed formatted xml erb</test>', @response.body
end

def test_should_render_formatted_html_erb_template
get :formatted_xml_erb
assert_equal '<test>passed formatted html erb</test>', @response.body
end

def test_should_render_formatted_html_erb_template_with_faulty_accepts_header
@request.env["HTTP_ACCEPT"] = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, appliction/x-shockwave-flash, */*"
get :formatted_xml_erb
assert_equal '<test>passed formatted html erb</test>', @response.body
end

def test_should_render_html_formatted_partial
get :partial
assert_equal 'partial html', @response.body
end

def test_should_render_html_partial_with_dot
get :partial_dot_html
assert_equal 'partial html', @response.body
end

def test_should_render_html_formatted_partial_with_rjs
xhr :get, :partial_as_rjs
assert_equal %(Element.replace("foo", "partial html");), @response.body
end

def test_should_render_html_formatted_partial_with_rjs_and_js_format
xhr :get, :respond_to_partial_as_rjs
assert_equal %(Element.replace("foo", "partial html");), @response.body
end

def test_should_render_js_partial
xhr :get, :partial, :format => 'js'
assert_equal 'partial js', @response.body
end

def test_should_render_with_alternate_default_render
xhr :get, :render_alternate_default
assert_equal %(Element.replace("foo", "partial html");), @response.body
end

def test_should_render_xml_but_keep_custom_content_type
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']
Expand Down Expand Up @@ -460,64 +530,40 @@ def test_etag_should_govern_renders_with_layouts_too
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
end

def test_should_render_formatted_xml_erb_template
get :formatted_xml_erb, :format => :xml
assert_equal '<test>passed formatted xml erb</test>', @response.body
end

def test_should_render_formatted_html_erb_template
get :formatted_xml_erb
assert_equal '<test>passed formatted html erb</test>', @response.body
end

def test_should_render_formatted_html_erb_template_with_faulty_accepts_header
@request.env["HTTP_ACCEPT"] = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, appliction/x-shockwave-flash, */*"
get :formatted_xml_erb
assert_equal '<test>passed formatted html erb</test>', @response.body
end

def test_should_render_html_formatted_partial
get :partial
assert_equal 'partial html', @response.body
end

def test_should_render_html_partial_with_dot
get :partial_dot_html
assert_equal 'partial html', @response.body
end
protected
def etag_for(text)
%("#{Digest::MD5.hexdigest(text)}")
end
end

def test_should_render_html_formatted_partial_with_rjs
xhr :get, :partial_as_rjs
assert_equal %(Element.replace("foo", "partial html");), @response.body
end
class LastModifiedRenderTest < Test::Unit::TestCase
def setup
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
@controller = TestController.new

def test_should_render_html_formatted_partial_with_rjs_and_js_format
xhr :get, :respond_to_partial_as_rjs
assert_equal %(Element.replace("foo", "partial html");), @response.body
@request.host = "www.nextangle.com"
@last_modified = Time.now.utc.beginning_of_day.httpdate
end

def test_should_render_js_partial
xhr :get, :partial, :format => 'js'
assert_equal 'partial js', @response.body
def test_responds_with_last_modified
get :conditional_hello
assert_equal @last_modified, @response.headers['Last-Modified']
end

def test_should_render_with_alternate_default_render
xhr :get, :render_alternate_default
assert_equal %(Element.replace("foo", "partial html");), @response.body
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_should_render_xml_but_keep_custom_content_type
get :render_xml_with_custom_content_type
assert_equal "application/atomsvc+xml", @response.content_type
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

protected
def etag_for(text)
%("#{Digest::MD5.hexdigest(text)}")
end
end

0 comments on commit 57a2780

Please sign in to comment.