public
Fork of rails/rails
Description: Ruby on Rails
Homepage: http://rubyonrails.org
Clone URL: git://github.com/jeremy/rails.git
etag! and last_modified! conditional GET helpers
jeremy (author)
Wed Jul 16 04:32:15 -0700 2008
commit  57a2780f14447152ece1b1301fc6c25b2ec43da5
tree    461185af231d685c355cc18b3e69584228dc1173
parent  a1fcbd971d681e44de5ea33e6a8470ff8b8144c0
...
1
2
 
 
 
 
3
4
5
...
1
2
3
4
5
6
7
8
9
0
@@ -1,5 +1,9 @@
0
 *Edge*
0
 
0
+* Conditional GET utility methods. [Jeremy Kemper]
0
+ * etag!([:admin, post, current_user]) sets the ETag response header and returns head(:not_modified) if it matches the If-None-Match request header.
0
+ * last_modified!(post.updated_at) sets Last-Modified and returns head(:not_modified) if it's no later than If-Modified-Since.
0
+
0
 * All 2xx requests are considered successful [Josh Peek]
0
 
0
 * 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]
...
519
520
521
 
 
522
523
524
...
529
530
531
532
533
534
535
536
...
968
969
970
 
 
 
 
 
 
 
 
 
 
 
971
972
973
...
519
520
521
522
523
524
525
526
...
531
532
533
 
 
534
535
536
...
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
0
@@ -519,6 +519,8 @@ module ActionController #:nodoc:
0
     public
0
       # Extracts the action_name from the request parameters and performs that action.
0
       def process(request, response, method = :perform_action, *arguments) #:nodoc:
0
+ response.request = request
0
+
0
         initialize_template_class(response)
0
         assign_shortcuts(request, response)
0
         initialize_current_url
0
@@ -529,8 +531,6 @@ module ActionController #:nodoc:
0
         send(method, *arguments)
0
 
0
         assign_default_content_type_and_charset
0
-
0
- response.request = request
0
         response.prepare! unless component_request?
0
         response
0
       ensure
0
@@ -968,6 +968,17 @@ module ActionController #:nodoc:
0
         render :nothing => true, :status => status
0
       end
0
 
0
+ # Sets the Last-Modified response header. Returns 304 Not Modified if the
0
+ # If-Modified-Since request header is <= last modified.
0
+ def last_modified!(utc_time)
0
+ head(:not_modified) if response.last_modified!(utc_time)
0
+ end
0
+
0
+ # Sets the ETag response header. Returns 304 Not Modified if the
0
+ # If-None-Match request header matches.
0
+ def etag!(etag)
0
+ head(:not_modified) if response.etag!(etag)
0
+ end
0
 
0
       # Clears the rendered results, allowing for another render to be performed.
0
       def erase_render_results #:nodoc:
...
41
42
43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
45
46
47
48
49
 
 
50
51
52
 
 
53
54
55
56
57
 
 
 
 
 
 
 
 
 
 
 
 
58
59
60
...
73
74
75
76
77
 
...
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
 
 
 
64
65
66
 
 
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
...
101
102
103
 
104
105
0
@@ -41,20 +41,48 @@ module ActionController
0
       set_content_length!
0
     end
0
 
0
+ # Sets the Last-Modified response header. Returns whether it's older than
0
+ # the If-Modified-Since request header.
0
+ def last_modified!(utc_time)
0
+ headers['Last-Modified'] ||= utc_time.httpdate
0
+ if request && since = request.headers['HTTP_IF_MODIFIED_SINCE']
0
+ utc_time <= Time.rfc2822(since)
0
+ end
0
+ end
0
+
0
+ # Sets the ETag response header. Returns whether it matches the
0
+ # If-None-Match request header.
0
+ def etag!(tag)
0
+ headers['ETag'] ||= %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(tag))}")
0
+ if request && request.headers['HTTP_IF_NONE_MATCH'] == headers['ETag']
0
+ true
0
+ end
0
+ end
0
 
0
     private
0
       def handle_conditional_get!
0
- if body.is_a?(String) && (headers['Status'] ? headers['Status'][0..2] == '200' : true) && !body.empty?
0
- self.headers['ETag'] ||= %("#{Digest::MD5.hexdigest(body)}")
0
- self.headers['Cache-Control'] = 'private, max-age=0, must-revalidate' if headers['Cache-Control'] == DEFAULT_HEADERS['Cache-Control']
0
+ if nonempty_ok_response?
0
+ set_conditional_cache_control!
0
 
0
- if request.headers['HTTP_IF_NONE_MATCH'] == headers['ETag']
0
- self.headers['Status'] = '304 Not Modified'
0
+ if etag!(body)
0
+ headers['Status'] = '304 Not Modified'
0
             self.body = ''
0
           end
0
         end
0
       end
0
 
0
+ def nonempty_ok_response?
0
+ status = headers['Status']
0
+ ok = !status || status[0..2] == '200'
0
+ ok && body.is_a?(String) && !body.empty?
0
+ end
0
+
0
+ def set_conditional_cache_control!
0
+ if headers['Cache-Control'] == DEFAULT_HEADERS['Cache-Control']
0
+ headers['Cache-Control'] = 'private, max-age=0, must-revalidate'
0
+ end
0
+ end
0
+
0
       def convert_content_type!
0
         if content_type = headers.delete("Content-Type")
0
           self.headers["type"] = content_type
0
@@ -73,4 +101,4 @@ module ActionController
0
         self.headers["Content-Length"] = body.size unless body.respond_to?(:call)
0
       end
0
   end
0
-end
0
\ No newline at end of file
0
+end
...
8
9
10
11
12
13
14
15
16
17
18
 
 
 
 
 
 
19
20
21
...
408
409
410
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411
412
413
...
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
 
 
 
 
 
493
494
495
496
497
 
 
 
 
 
498
499
500
501
 
 
502
503
504
505
506
 
 
 
507
508
509
510
511
 
 
 
 
 
 
512
513
514
515
516
 
 
 
 
 
 
517
518
519
520
521
522
523
...
8
9
10
 
 
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
...
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
...
530
531
532
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
533
534
535
536
537
538
 
 
 
 
539
540
541
542
543
544
 
 
 
545
546
547
548
 
 
 
549
550
551
552
553
 
 
 
554
555
556
557
558
559
560
561
 
 
 
562
563
564
565
566
567
568
 
 
 
 
 
569
0
@@ -8,14 +8,18 @@ module Fun
0
   end
0
 end
0
 
0
-
0
-# FIXME: crashes Ruby 1.9
0
 class TestController < ActionController::Base
0
   layout :determine_layout
0
 
0
   def hello_world
0
   end
0
 
0
+ def conditional_hello
0
+ etag! [:foo, 123]
0
+ last_modified! Time.now.utc.beginning_of_day
0
+ render :action => 'hello_world' unless performed?
0
+ end
0
+
0
   def render_hello_world
0
     render :template => "test/hello_world"
0
   end
0
@@ -408,6 +412,72 @@ class RenderTest < Test::Unit::TestCase
0
     assert_equal "Goodbye, Local David", @response.body
0
   end
0
 
0
+ def test_should_render_formatted_template
0
+ get :formatted_html_erb
0
+ assert_equal 'formatted html erb', @response.body
0
+ end
0
+
0
+ def test_should_render_formatted_xml_erb_template
0
+ get :formatted_xml_erb, :format => :xml
0
+ assert_equal '<test>passed formatted xml erb</test>', @response.body
0
+ end
0
+
0
+ def test_should_render_formatted_html_erb_template
0
+ get :formatted_xml_erb
0
+ assert_equal '<test>passed formatted html erb</test>', @response.body
0
+ end
0
+
0
+ def test_should_render_formatted_html_erb_template_with_faulty_accepts_header
0
+ @request.env["HTTP_ACCEPT"] = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, appliction/x-shockwave-flash, */*"
0
+ get :formatted_xml_erb
0
+ assert_equal '<test>passed formatted html erb</test>', @response.body
0
+ end
0
+
0
+ def test_should_render_html_formatted_partial
0
+ get :partial
0
+ assert_equal 'partial html', @response.body
0
+ end
0
+
0
+ def test_should_render_html_partial_with_dot
0
+ get :partial_dot_html
0
+ assert_equal 'partial html', @response.body
0
+ end
0
+
0
+ def test_should_render_html_formatted_partial_with_rjs
0
+ xhr :get, :partial_as_rjs
0
+ assert_equal %(Element.replace("foo", "partial html");), @response.body
0
+ end
0
+
0
+ def test_should_render_html_formatted_partial_with_rjs_and_js_format
0
+ xhr :get, :respond_to_partial_as_rjs
0
+ assert_equal %(Element.replace("foo", "partial html");), @response.body
0
+ end
0
+
0
+ def test_should_render_js_partial
0
+ xhr :get, :partial, :format => 'js'
0
+ assert_equal 'partial js', @response.body
0
+ end
0
+
0
+ def test_should_render_with_alternate_default_render
0
+ xhr :get, :render_alternate_default
0
+ assert_equal %(Element.replace("foo", "partial html");), @response.body
0
+ end
0
+
0
+ def test_should_render_xml_but_keep_custom_content_type
0
+ get :render_xml_with_custom_content_type
0
+ assert_equal "application/atomsvc+xml", @response.content_type
0
+ end
0
+end
0
+
0
+class EtagRenderTest < Test::Unit::TestCase
0
+ def setup
0
+ @request = ActionController::TestRequest.new
0
+ @response = ActionController::TestResponse.new
0
+ @controller = TestController.new
0
+
0
+ @request.host = "www.nextangle.com"
0
+ end
0
+
0
   def test_render_200_should_set_etag
0
     get :render_hello_world_from_variable
0
     assert_equal etag_for("hello david"), @response.headers['ETag']
0
@@ -460,64 +530,40 @@ class RenderTest < Test::Unit::TestCase
0
     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']
0
   end
0
 
0
- def test_should_render_formatted_template
0
- get :formatted_html_erb
0
- assert_equal 'formatted html erb', @response.body
0
- end
0
-
0
- def test_should_render_formatted_xml_erb_template
0
- get :formatted_xml_erb, :format => :xml
0
- assert_equal '<test>passed formatted xml erb</test>', @response.body
0
- end
0
-
0
- def test_should_render_formatted_html_erb_template
0
- get :formatted_xml_erb
0
- assert_equal '<test>passed formatted html erb</test>', @response.body
0
- end
0
-
0
- def test_should_render_formatted_html_erb_template_with_faulty_accepts_header
0
- @request.env["HTTP_ACCEPT"] = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, appliction/x-shockwave-flash, */*"
0
- get :formatted_xml_erb
0
- assert_equal '<test>passed formatted html erb</test>', @response.body
0
- end
0
-
0
- def test_should_render_html_formatted_partial
0
- get :partial
0
- assert_equal 'partial html', @response.body
0
- end
0
-
0
- def test_should_render_html_partial_with_dot
0
- get :partial_dot_html
0
- assert_equal 'partial html', @response.body
0
- end
0
+ protected
0
+ def etag_for(text)
0
+ %("#{Digest::MD5.hexdigest(text)}")
0
+ end
0
+end
0
 
0
- def test_should_render_html_formatted_partial_with_rjs
0
- xhr :get, :partial_as_rjs
0
- assert_equal %(Element.replace("foo", "partial html");), @response.body
0
- end
0
+class LastModifiedRenderTest < Test::Unit::TestCase
0
+ def setup
0
+ @request = ActionController::TestRequest.new
0
+ @response = ActionController::TestResponse.new
0
+ @controller = TestController.new
0
 
0
- def test_should_render_html_formatted_partial_with_rjs_and_js_format
0
- xhr :get, :respond_to_partial_as_rjs
0
- assert_equal %(Element.replace("foo", "partial html");), @response.body
0
+ @request.host = "www.nextangle.com"
0
+ @last_modified = Time.now.utc.beginning_of_day.httpdate
0
   end
0
 
0
- def test_should_render_js_partial
0
- xhr :get, :partial, :format => 'js'
0
- assert_equal 'partial js', @response.body
0
+ def test_responds_with_last_modified
0
+ get :conditional_hello
0
+ assert_equal @last_modified, @response.headers['Last-Modified']
0
   end
0
 
0
- def test_should_render_with_alternate_default_render
0
- xhr :get, :render_alternate_default
0
- assert_equal %(Element.replace("foo", "partial html");), @response.body
0
+ def test_request_not_modified
0
+ @request.headers["HTTP_IF_MODIFIED_SINCE"] = @last_modified
0
+ get :conditional_hello
0
+ assert_equal "304 Not Modified", @response.headers['Status']
0
+ assert @response.body.blank?, @response.body
0
+ assert_equal @last_modified, @response.headers['Last-Modified']
0
   end
0
 
0
- def test_should_render_xml_but_keep_custom_content_type
0
- get :render_xml_with_custom_content_type
0
- assert_equal "application/atomsvc+xml", @response.content_type
0
+ def test_request_modified
0
+ @request.headers["HTTP_IF_MODIFIED_SINCE"] = 'Thu, 16 Jul 2008 00:00:00 GMT'
0
+ get :conditional_hello
0
+ assert_equal "200 OK", @response.headers['Status']
0
+ assert !@response.body.blank?
0
+ assert_equal @last_modified, @response.headers['Last-Modified']
0
   end
0
-
0
- protected
0
- def etag_for(text)
0
- %("#{Digest::MD5.hexdigest(text)}")
0
- end
0
 end

Comments

    No one has commented yet.