actionpack 7.0.10 → 7.1.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of actionpack might be problematic. Click here for more details.

Files changed (135) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +318 -452
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +2 -2
  5. data/lib/abstract_controller/base.rb +19 -10
  6. data/lib/abstract_controller/caching/fragments.rb +2 -0
  7. data/lib/abstract_controller/callbacks.rb +31 -6
  8. data/lib/abstract_controller/deprecator.rb +7 -0
  9. data/lib/abstract_controller/helpers.rb +61 -18
  10. data/lib/abstract_controller/railties/routes_helpers.rb +1 -16
  11. data/lib/abstract_controller/rendering.rb +3 -3
  12. data/lib/abstract_controller/translation.rb +1 -27
  13. data/lib/abstract_controller/url_for.rb +2 -0
  14. data/lib/abstract_controller.rb +6 -0
  15. data/lib/action_controller/api.rb +5 -3
  16. data/lib/action_controller/base.rb +3 -17
  17. data/lib/action_controller/caching.rb +2 -0
  18. data/lib/action_controller/deprecator.rb +7 -0
  19. data/lib/action_controller/form_builder.rb +2 -0
  20. data/lib/action_controller/log_subscriber.rb +16 -4
  21. data/lib/action_controller/metal/content_security_policy.rb +1 -1
  22. data/lib/action_controller/metal/data_streaming.rb +2 -0
  23. data/lib/action_controller/metal/default_headers.rb +2 -0
  24. data/lib/action_controller/metal/etag_with_flash.rb +2 -0
  25. data/lib/action_controller/metal/etag_with_template_digest.rb +2 -0
  26. data/lib/action_controller/metal/exceptions.rb +8 -0
  27. data/lib/action_controller/metal/head.rb +8 -6
  28. data/lib/action_controller/metal/helpers.rb +3 -14
  29. data/lib/action_controller/metal/http_authentication.rb +11 -5
  30. data/lib/action_controller/metal/implicit_render.rb +5 -3
  31. data/lib/action_controller/metal/instrumentation.rb +8 -1
  32. data/lib/action_controller/metal/live.rb +24 -0
  33. data/lib/action_controller/metal/mime_responds.rb +2 -2
  34. data/lib/action_controller/metal/params_wrapper.rb +4 -2
  35. data/lib/action_controller/metal/permissions_policy.rb +1 -1
  36. data/lib/action_controller/metal/redirecting.rb +7 -7
  37. data/lib/action_controller/metal/renderers.rb +2 -2
  38. data/lib/action_controller/metal/rendering.rb +0 -7
  39. data/lib/action_controller/metal/request_forgery_protection.rb +138 -50
  40. data/lib/action_controller/metal/rescue.rb +2 -0
  41. data/lib/action_controller/metal/streaming.rb +70 -30
  42. data/lib/action_controller/metal/strong_parameters.rb +89 -50
  43. data/lib/action_controller/metal/url_for.rb +7 -0
  44. data/lib/action_controller/metal.rb +79 -21
  45. data/lib/action_controller/railtie.rb +22 -9
  46. data/lib/action_controller/renderer.rb +98 -65
  47. data/lib/action_controller/test_case.rb +15 -5
  48. data/lib/action_controller.rb +8 -1
  49. data/lib/action_dispatch/constants.rb +32 -0
  50. data/lib/action_dispatch/deprecator.rb +7 -0
  51. data/lib/action_dispatch/http/cache.rb +1 -3
  52. data/lib/action_dispatch/http/content_security_policy.rb +13 -29
  53. data/lib/action_dispatch/http/filter_parameters.rb +15 -14
  54. data/lib/action_dispatch/http/headers.rb +2 -0
  55. data/lib/action_dispatch/http/mime_negotiation.rb +22 -22
  56. data/lib/action_dispatch/http/mime_type.rb +35 -12
  57. data/lib/action_dispatch/http/mime_types.rb +3 -1
  58. data/lib/action_dispatch/http/parameters.rb +1 -1
  59. data/lib/action_dispatch/http/permissions_policy.rb +45 -16
  60. data/lib/action_dispatch/http/rack_cache.rb +2 -0
  61. data/lib/action_dispatch/http/request.rb +48 -14
  62. data/lib/action_dispatch/http/response.rb +78 -59
  63. data/lib/action_dispatch/http/upload.rb +2 -0
  64. data/lib/action_dispatch/journey/formatter.rb +8 -2
  65. data/lib/action_dispatch/journey/path/pattern.rb +14 -14
  66. data/lib/action_dispatch/journey/route.rb +3 -2
  67. data/lib/action_dispatch/journey/router.rb +5 -4
  68. data/lib/action_dispatch/journey/routes.rb +2 -2
  69. data/lib/action_dispatch/log_subscriber.rb +23 -0
  70. data/lib/action_dispatch/middleware/actionable_exceptions.rb +5 -6
  71. data/lib/action_dispatch/middleware/assume_ssl.rb +24 -0
  72. data/lib/action_dispatch/middleware/callbacks.rb +2 -0
  73. data/lib/action_dispatch/middleware/cookies.rb +81 -98
  74. data/lib/action_dispatch/middleware/debug_exceptions.rb +26 -25
  75. data/lib/action_dispatch/middleware/debug_locks.rb +4 -1
  76. data/lib/action_dispatch/middleware/debug_view.rb +7 -2
  77. data/lib/action_dispatch/middleware/exception_wrapper.rb +181 -27
  78. data/lib/action_dispatch/middleware/executor.rb +1 -7
  79. data/lib/action_dispatch/middleware/flash.rb +7 -0
  80. data/lib/action_dispatch/middleware/host_authorization.rb +6 -3
  81. data/lib/action_dispatch/middleware/public_exceptions.rb +5 -3
  82. data/lib/action_dispatch/middleware/reloader.rb +7 -5
  83. data/lib/action_dispatch/middleware/remote_ip.rb +17 -16
  84. data/lib/action_dispatch/middleware/request_id.rb +2 -0
  85. data/lib/action_dispatch/middleware/server_timing.rb +4 -4
  86. data/lib/action_dispatch/middleware/session/abstract_store.rb +5 -0
  87. data/lib/action_dispatch/middleware/session/cache_store.rb +2 -0
  88. data/lib/action_dispatch/middleware/session/cookie_store.rb +11 -5
  89. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +3 -1
  90. data/lib/action_dispatch/middleware/show_exceptions.rb +19 -16
  91. data/lib/action_dispatch/middleware/ssl.rb +18 -6
  92. data/lib/action_dispatch/middleware/stack.rb +7 -2
  93. data/lib/action_dispatch/middleware/static.rb +12 -8
  94. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +2 -2
  95. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +4 -4
  96. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +8 -1
  97. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +7 -7
  98. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +2 -2
  99. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +17 -0
  100. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +16 -12
  101. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +1 -1
  102. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +3 -3
  103. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +4 -4
  104. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -1
  105. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +1 -1
  106. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +3 -0
  107. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +46 -37
  108. data/lib/action_dispatch/railtie.rb +14 -4
  109. data/lib/action_dispatch/request/session.rb +16 -6
  110. data/lib/action_dispatch/request/utils.rb +8 -3
  111. data/lib/action_dispatch/routing/inspector.rb +54 -6
  112. data/lib/action_dispatch/routing/mapper.rb +26 -14
  113. data/lib/action_dispatch/routing/polymorphic_routes.rb +2 -0
  114. data/lib/action_dispatch/routing/redirection.rb +15 -6
  115. data/lib/action_dispatch/routing/route_set.rb +52 -22
  116. data/lib/action_dispatch/routing/routes_proxy.rb +1 -1
  117. data/lib/action_dispatch/routing/url_for.rb +5 -1
  118. data/lib/action_dispatch/routing.rb +4 -4
  119. data/lib/action_dispatch/system_test_case.rb +3 -3
  120. data/lib/action_dispatch/system_testing/browser.rb +5 -6
  121. data/lib/action_dispatch/system_testing/driver.rb +13 -21
  122. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +27 -16
  123. data/lib/action_dispatch/testing/assertions/response.rb +13 -6
  124. data/lib/action_dispatch/testing/assertions/routing.rb +67 -28
  125. data/lib/action_dispatch/testing/assertions.rb +3 -1
  126. data/lib/action_dispatch/testing/integration.rb +27 -17
  127. data/lib/action_dispatch/testing/request_encoder.rb +4 -1
  128. data/lib/action_dispatch/testing/test_process.rb +4 -3
  129. data/lib/action_dispatch/testing/test_request.rb +1 -1
  130. data/lib/action_dispatch/testing/test_response.rb +23 -9
  131. data/lib/action_dispatch.rb +37 -4
  132. data/lib/action_pack/gem_version.rb +4 -4
  133. data/lib/action_pack/version.rb +1 -1
  134. data/lib/action_pack.rb +1 -1
  135. metadata +44 -33
@@ -11,6 +11,8 @@ module ActionController # :nodoc:
11
11
  class InvalidCrossOriginRequest < ActionControllerError # :nodoc:
12
12
  end
13
13
 
14
+ # = Action Controller Request Forgery Protection
15
+ #
14
16
  # Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks
15
17
  # by including a token in the rendered HTML for your application. This token is
16
18
  # stored as a random string in the session, to which an attacker does not have
@@ -34,10 +36,10 @@ module ActionController # :nodoc:
34
36
  #
35
37
  # Subclasses of ActionController::Base are protected by default with the
36
38
  # <tt>:exception</tt> strategy, which raises an
37
- # <tt>ActionController::InvalidAuthenticityToken</tt> error on unverified requests.
39
+ # ActionController::InvalidAuthenticityToken error on unverified requests.
38
40
  #
39
41
  # APIs may want to disable this behavior since they are typically designed to be
40
- # state-less: that is, the request API client handles the session instead of Rails.
42
+ # state-less: that is, the request API client handles the session instead of \Rails.
41
43
  # One way to achieve this is to use the <tt>:null_session</tt> strategy instead,
42
44
  # which allows unverified requests to be handled, but with an empty session:
43
45
  #
@@ -55,6 +57,8 @@ module ActionController # :nodoc:
55
57
  # Learn more about CSRF attacks and securing your application in the
56
58
  # {Ruby on Rails Security Guide}[https://guides.rubyonrails.org/security.html].
57
59
  module RequestForgeryProtection
60
+ CSRF_TOKEN = "action_controller.csrf_token"
61
+
58
62
  extend ActiveSupport::Concern
59
63
 
60
64
  include AbstractController::Helpers
@@ -90,18 +94,9 @@ module ActionController # :nodoc:
90
94
  config_accessor :default_protect_from_forgery
91
95
  self.default_protect_from_forgery = false
92
96
 
93
- # Controls whether URL-safe CSRF tokens are generated.
94
- config_accessor :urlsafe_csrf_tokens, instance_writer: false
95
- self.urlsafe_csrf_tokens = true
96
-
97
- singleton_class.redefine_method(:urlsafe_csrf_tokens=) do |urlsafe_csrf_tokens|
98
- if urlsafe_csrf_tokens
99
- ActiveSupport::Deprecation.warn("URL-safe CSRF tokens are now the default. Use 6.1 defaults or above.")
100
- else
101
- ActiveSupport::Deprecation.warn("Non-URL-safe CSRF tokens are deprecated. Use 6.1 defaults or above.")
102
- end
103
- config.urlsafe_csrf_tokens = urlsafe_csrf_tokens
104
- end
97
+ # The strategy to use for storing and retrieving CSRF tokens.
98
+ config_accessor :csrf_token_storage_strategy
99
+ self.csrf_token_storage_strategy = SessionStore.new
105
100
 
106
101
  helper_method :form_authenticity_token
107
102
  helper_method :protect_against_forgery?
@@ -148,18 +143,46 @@ module ActionController # :nodoc:
148
143
  # end
149
144
  #
150
145
  # def handle_unverified_request
151
- # # Custom behaviour for unverfied request
146
+ # # Custom behavior for unverfied request
152
147
  # end
153
148
  # end
154
149
  #
155
- # class ApplicationController < ActionController:x:Base
150
+ # class ApplicationController < ActionController::Base
156
151
  # protect_from_forgery with: CustomStrategy
157
152
  # end
153
+ # * <tt>:store</tt> - Set the strategy to store and retrieve CSRF tokens.
154
+ #
155
+ # Built-in session token strategies are:
156
+ # * <tt>:session</tt> - Store the CSRF token in the session. Used as default if <tt>:store</tt> option is not specified.
157
+ # * <tt>:cookie</tt> - Store the CSRF token in an encrypted cookie.
158
+ #
159
+ # You can also implement custom strategy classes for CSRF token storage:
160
+ #
161
+ # class CustomStore
162
+ # def fetch(request)
163
+ # # Return the token from a custom location
164
+ # end
165
+ #
166
+ # def store(request, csrf_token)
167
+ # # Store the token in a custom location
168
+ # end
169
+ #
170
+ # def reset(request)
171
+ # # Delete the stored session token
172
+ # end
173
+ # end
174
+ #
175
+ # class ApplicationController < ActionController::Base
176
+ # protect_from_forgery store: CustomStore.new
177
+ # end
158
178
  def protect_from_forgery(options = {})
159
179
  options = options.reverse_merge(prepend: false)
160
180
 
161
181
  self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session)
162
182
  self.request_forgery_protection_token ||= :authenticity_token
183
+
184
+ self.csrf_token_storage_strategy = storage_strategy(options[:store] || SessionStore.new)
185
+
163
186
  before_action :verify_authenticity_token, options
164
187
  append_after_action :verify_same_origin_request
165
188
  end
@@ -188,6 +211,22 @@ module ActionController # :nodoc:
188
211
  raise ArgumentError, "Invalid request forgery protection method, use :null_session, :exception, :reset_session, or a custom forgery protection class."
189
212
  end
190
213
  end
214
+
215
+ def storage_strategy(name)
216
+ case name
217
+ when :session
218
+ SessionStore.new
219
+ when :cookie
220
+ CookieStore.new(:csrf_token)
221
+ else
222
+ return name if is_storage_strategy?(name)
223
+ raise ArgumentError, "Invalid CSRF token storage strategy, use :session, :cookie, or a custom CSRF token storage class."
224
+ end
225
+ end
226
+
227
+ def is_storage_strategy?(object)
228
+ object.respond_to?(:fetch) && object.respond_to?(:store) && object.respond_to?(:reset)
229
+ end
191
230
  end
192
231
 
193
232
  module ProtectionMethods
@@ -255,6 +294,68 @@ module ActionController # :nodoc:
255
294
  end
256
295
  end
257
296
 
297
+ class SessionStore
298
+ def fetch(request)
299
+ request.session[:_csrf_token]
300
+ end
301
+
302
+ def store(request, csrf_token)
303
+ request.session[:_csrf_token] = csrf_token
304
+ end
305
+
306
+ def reset(request)
307
+ request.session.delete(:_csrf_token)
308
+ end
309
+ end
310
+
311
+ class CookieStore
312
+ def initialize(cookie = :csrf_token)
313
+ @cookie_name = cookie
314
+ end
315
+
316
+ def fetch(request)
317
+ contents = request.cookie_jar.encrypted[@cookie_name]
318
+ return nil if contents.nil?
319
+
320
+ value = JSON.parse(contents)
321
+ return nil unless value.dig("session_id", "public_id") == request.session.id_was&.public_id
322
+
323
+ value["token"]
324
+ rescue JSON::ParserError
325
+ nil
326
+ end
327
+
328
+ def store(request, csrf_token)
329
+ request.cookie_jar.encrypted.permanent[@cookie_name] = {
330
+ value: {
331
+ token: csrf_token,
332
+ session_id: request.session.id,
333
+ }.to_json,
334
+ httponly: true,
335
+ same_site: :lax,
336
+ }
337
+ end
338
+
339
+ def reset(request)
340
+ request.cookie_jar.delete(@cookie_name)
341
+ end
342
+ end
343
+
344
+ def initialize(...)
345
+ super
346
+ @marked_for_same_origin_verification = nil
347
+ end
348
+
349
+ def reset_csrf_token(request) # :doc:
350
+ request.env.delete(CSRF_TOKEN)
351
+ csrf_token_storage_strategy.reset(request)
352
+ end
353
+
354
+ def commit_csrf_token(request) # :doc:
355
+ csrf_token = request.env[CSRF_TOKEN]
356
+ csrf_token_storage_strategy.store(request, csrf_token) unless csrf_token.nil?
357
+ end
358
+
258
359
  private
259
360
  # The actual before_action that is used to verify the CSRF token.
260
361
  # Don't override this directly. Provide your own forgery protection
@@ -356,20 +457,20 @@ module ActionController # :nodoc:
356
457
 
357
458
  # Creates the authenticity token for the current request.
358
459
  def form_authenticity_token(form_options: {}) # :doc:
359
- masked_authenticity_token(session, form_options: form_options)
460
+ masked_authenticity_token(form_options: form_options)
360
461
  end
361
462
 
362
463
  # Creates a masked version of the authenticity token that varies
363
464
  # on each request. The masking is used to mitigate SSL attacks
364
465
  # like BREACH.
365
- def masked_authenticity_token(session, form_options: {})
466
+ def masked_authenticity_token(form_options: {})
366
467
  action, method = form_options.values_at(:action, :method)
367
468
 
368
469
  raw_token = if per_form_csrf_tokens && action && method
369
470
  action_path = normalize_action_path(action)
370
- per_form_csrf_token(session, action_path, method)
471
+ per_form_csrf_token(nil, action_path, method)
371
472
  else
372
- global_csrf_token(session)
473
+ global_csrf_token
373
474
  end
374
475
 
375
476
  mask_token(raw_token)
@@ -397,14 +498,14 @@ module ActionController # :nodoc:
397
498
  # This is actually an unmasked token. This is expected if
398
499
  # you have just upgraded to masked tokens, but should stop
399
500
  # happening shortly after installing this gem.
400
- compare_with_real_token masked_token, session
501
+ compare_with_real_token masked_token
401
502
 
402
503
  elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
403
504
  csrf_token = unmask_token(masked_token)
404
505
 
405
- compare_with_global_token(csrf_token, session) ||
406
- compare_with_real_token(csrf_token, session) ||
407
- valid_per_form_csrf_token?(csrf_token, session)
506
+ compare_with_global_token(csrf_token) ||
507
+ compare_with_real_token(csrf_token) ||
508
+ valid_per_form_csrf_token?(csrf_token)
408
509
  else
409
510
  false # Token is malformed.
410
511
  end
@@ -425,15 +526,15 @@ module ActionController # :nodoc:
425
526
  encode_csrf_token(masked_token)
426
527
  end
427
528
 
428
- def compare_with_real_token(token, session) # :doc:
529
+ def compare_with_real_token(token, session = nil) # :doc:
429
530
  ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, real_csrf_token(session))
430
531
  end
431
532
 
432
- def compare_with_global_token(token, session) # :doc:
533
+ def compare_with_global_token(token, session = nil) # :doc:
433
534
  ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, global_csrf_token(session))
434
535
  end
435
536
 
436
- def valid_per_form_csrf_token?(token, session) # :doc:
537
+ def valid_per_form_csrf_token?(token, session = nil) # :doc:
437
538
  if per_form_csrf_tokens
438
539
  correct_token = per_form_csrf_token(
439
540
  session,
@@ -447,9 +548,12 @@ module ActionController # :nodoc:
447
548
  end
448
549
  end
449
550
 
450
- def real_csrf_token(session) # :doc:
451
- session[:_csrf_token] ||= generate_csrf_token
452
- decode_csrf_token(session[:_csrf_token])
551
+ def real_csrf_token(_session = nil) # :doc:
552
+ csrf_token = request.env.fetch(CSRF_TOKEN) do
553
+ request.env[CSRF_TOKEN] = csrf_token_storage_strategy.fetch(request) || generate_csrf_token
554
+ end
555
+
556
+ decode_csrf_token(csrf_token)
453
557
  end
454
558
 
455
559
  def per_form_csrf_token(session, action_path, method) # :doc:
@@ -459,7 +563,7 @@ module ActionController # :nodoc:
459
563
  GLOBAL_CSRF_TOKEN_IDENTIFIER = "!real_csrf_token"
460
564
  private_constant :GLOBAL_CSRF_TOKEN_IDENTIFIER
461
565
 
462
- def global_csrf_token(session) # :doc:
566
+ def global_csrf_token(session = nil) # :doc:
463
567
  csrf_token_hmac(session, GLOBAL_CSRF_TOKEN_IDENTIFIER)
464
568
  end
465
569
 
@@ -519,31 +623,15 @@ module ActionController # :nodoc:
519
623
  end
520
624
 
521
625
  def generate_csrf_token # :nodoc:
522
- if urlsafe_csrf_tokens
523
- SecureRandom.urlsafe_base64(AUTHENTICITY_TOKEN_LENGTH)
524
- else
525
- SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
526
- end
626
+ SecureRandom.urlsafe_base64(AUTHENTICITY_TOKEN_LENGTH)
527
627
  end
528
628
 
529
629
  def encode_csrf_token(csrf_token) # :nodoc:
530
- if urlsafe_csrf_tokens
531
- Base64.urlsafe_encode64(csrf_token, padding: false)
532
- else
533
- Base64.strict_encode64(csrf_token)
534
- end
630
+ Base64.urlsafe_encode64(csrf_token, padding: false)
535
631
  end
536
632
 
537
633
  def decode_csrf_token(encoded_csrf_token) # :nodoc:
538
- if urlsafe_csrf_tokens
539
- Base64.urlsafe_decode64(encoded_csrf_token)
540
- else
541
- begin
542
- Base64.strict_decode64(encoded_csrf_token)
543
- rescue ArgumentError
544
- Base64.urlsafe_decode64(encoded_csrf_token)
545
- end
546
- end
634
+ Base64.urlsafe_decode64(encoded_csrf_token)
547
635
  end
548
636
  end
549
637
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionController # :nodoc:
4
+ # = Action Controller \Rescue
5
+ #
4
6
  # This module is responsible for providing
5
7
  # {rescue_from}[rdoc-ref:ActiveSupport::Rescuable::ClassMethods#rescue_from]
6
8
  # to controllers, wrapping actions to handle configured errors, and
@@ -1,30 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rack/chunked"
4
-
5
3
  module ActionController # :nodoc:
4
+ # = Action Controller \Streaming
5
+ #
6
6
  # Allows views to be streamed back to the client as they are rendered.
7
7
  #
8
- # By default, Rails renders views by first rendering the template
8
+ # By default, \Rails renders views by first rendering the template
9
9
  # and then the layout. The response is sent to the client after the whole
10
10
  # template is rendered, all queries are made, and the layout is processed.
11
11
  #
12
- # Streaming inverts the rendering flow by rendering the layout first and
13
- # streaming each part of the layout as they are processed. This allows the
12
+ # \Streaming inverts the rendering flow by rendering the layout first and
13
+ # subsequently each part of the layout as they are processed. This allows the
14
14
  # header of the HTML (which is usually in the layout) to be streamed back
15
- # to client very quickly, allowing JavaScripts and stylesheets to be loaded
15
+ # to client very quickly, enabling JavaScripts and stylesheets to be loaded
16
16
  # earlier than usual.
17
17
  #
18
- # This approach was introduced in Rails 3.1 and is still improving. Several
19
- # Rack middlewares may not work and you need to be careful when streaming.
20
- # Those points are going to be addressed soon.
21
- #
22
- # In order to use streaming, you will need to use a Ruby version that
23
- # supports fibers (fibers are supported since version 1.9.2 of the main
24
- # Ruby implementation).
18
+ # Several Rack middlewares may not work and you need to be careful when streaming.
19
+ # This is covered in more detail below, see the Streaming@Middlewares section.
25
20
  #
26
- # Streaming can be added to a given template easily, all you need to do is
27
- # to pass the +:stream+ option.
21
+ # \Streaming can be added to a given template easily, all you need to do is
22
+ # to pass the +:stream+ option to +render+.
28
23
  #
29
24
  # class PostsController
30
25
  # def index
@@ -35,7 +30,7 @@ module ActionController # :nodoc:
35
30
  #
36
31
  # == When to use streaming
37
32
  #
38
- # Streaming may be considered to be overkill for lightweight actions like
33
+ # \Streaming may be considered to be overkill for lightweight actions like
39
34
  # +new+ or +edit+. The real benefit of streaming is on expensive actions
40
35
  # that, for example, do a lot of queries on the database.
41
36
  #
@@ -59,13 +54,13 @@ module ActionController # :nodoc:
59
54
  # render stream: true
60
55
  # end
61
56
  #
62
- # Notice that +:stream+ only works with templates. Rendering +:json+
57
+ # Notice that +:stream+ only works with templates. \Rendering +:json+
63
58
  # or +:xml+ with +:stream+ won't work.
64
59
  #
65
60
  # == Communication between layout and template
66
61
  #
67
62
  # When streaming, rendering happens top-down instead of inside-out.
68
- # Rails starts with the layout, and the template is rendered later,
63
+ # \Rails starts with the layout, and the template is rendered later,
69
64
  # when its +yield+ is reached.
70
65
  #
71
66
  # This means that, if your application currently relies on instance
@@ -112,7 +107,7 @@ module ActionController # :nodoc:
112
107
  # This means that, if you have <code>yield :title</code> in your layout
113
108
  # and you want to use streaming, you would have to render the whole template
114
109
  # (and eventually trigger all queries) before streaming the title and all
115
- # assets, which kills the purpose of streaming. For this purpose, you can use
110
+ # assets, which defeats the purpose of streaming. Alternatively, you can use
116
111
  # a helper called +provide+ that does the same as +content_for+ but tells the
117
112
  # layout to stop searching for other entries and continue rendering.
118
113
  #
@@ -122,7 +117,7 @@ module ActionController # :nodoc:
122
117
  # Hello
123
118
  # <%= content_for :title, " page" %>
124
119
  #
125
- # Giving:
120
+ # Resulting in:
126
121
  #
127
122
  # <html>
128
123
  # <head><title>Main</title></head>
@@ -132,6 +127,8 @@ module ActionController # :nodoc:
132
127
  # That said, when streaming, you need to properly check your templates
133
128
  # and choose when to use +provide+ and +content_for+.
134
129
  #
130
+ # See also ActionView::Helpers::CaptureHelper for more information.
131
+ #
135
132
  # == Headers, cookies, session, and flash
136
133
  #
137
134
  # When streaming, the HTTP headers are sent to the client right before
@@ -143,10 +140,10 @@ module ActionController # :nodoc:
143
140
  #
144
141
  # Middlewares that need to manipulate the body won't work with streaming.
145
142
  # You should disable those middlewares whenever streaming in development
146
- # or production. For instance, <tt>Rack::Bug</tt> won't work when streaming as it
143
+ # or production. For instance, +Rack::Bug+ won't work when streaming as it
147
144
  # needs to inject contents in the HTML body.
148
145
  #
149
- # Also <tt>Rack::Cache</tt> won't work with streaming as it does not support
146
+ # Also +Rack::Cache+ won't work with streaming as it does not support
150
147
  # streaming bodies yet. Whenever streaming +Cache-Control+ is automatically
151
148
  # set to "no-cache".
152
149
  #
@@ -156,14 +153,14 @@ module ActionController # :nodoc:
156
153
  # happens because part of the template was already rendered and streamed to
157
154
  # the client, making it impossible to render a whole exception page.
158
155
  #
159
- # Currently, when an exception happens in development or production, Rails
156
+ # Currently, when an exception happens in development or production, \Rails
160
157
  # will automatically stream to the client:
161
158
  #
162
159
  # "><script>window.location = "/500.html"</script></html>
163
160
  #
164
- # The first two characters (">) are required in case the exception happens
165
- # while rendering attributes for a given tag. You can check the real cause
166
- # for the exception in your logger.
161
+ # The first two characters (<tt>"></tt>) are required in case the exception
162
+ # happens while rendering attributes for a given tag. You can check the real
163
+ # cause for the exception in your logger.
167
164
  #
168
165
  # == Web server support
169
166
  #
@@ -183,16 +180,59 @@ module ActionController # :nodoc:
183
180
  # unicorn_rails --config-file unicorn.config.rb
184
181
  #
185
182
  # You may also want to configure other parameters like <tt>:tcp_nodelay</tt>.
186
- # Please check its documentation for more information: https://bogomips.org/unicorn/Unicorn/Configurator.html#method-i-listen
183
+ #
184
+ # For more information, please check the
185
+ # {documentation}[https://bogomips.org/unicorn/Unicorn/Configurator.html#method-i-listen].
187
186
  #
188
187
  # If you are using Unicorn with NGINX, you may need to tweak NGINX.
189
- # Streaming should work out of the box on Rainbows.
188
+ # \Streaming should work out of the box on Rainbows.
190
189
  #
191
190
  # ==== Passenger
192
191
  #
193
- # To be described.
192
+ # Phusion Passenger with NGINX, offers two streaming mechanisms out of the box.
193
+ #
194
+ # 1. NGINX response buffering mechanism which is dependent on the value of
195
+ # +passenger_buffer_response+ option (default is "off").
196
+ # 2. Passenger buffering system which is always 'on' irrespective of the value
197
+ # of +passenger_buffer_response+.
198
+ #
199
+ # When +passenger_buffer_response+ is turned "on", then streaming would be
200
+ # done at the NGINX level which waits until the application is done sending
201
+ # the response back to the client.
202
+ #
203
+ # For more information, please check the
204
+ # {documentation}[https://www.phusionpassenger.com/docs/references/config_reference/nginx/#passenger_buffer_response].
194
205
  #
195
206
  module Streaming
207
+ class Body # :nodoc:
208
+ TERM = "\r\n"
209
+ TAIL = "0#{TERM}"
210
+
211
+ # Store the response body to be chunked.
212
+ def initialize(body)
213
+ @body = body
214
+ end
215
+
216
+ # For each element yielded by the response body, yield
217
+ # the element in chunked encoding.
218
+ def each(&block)
219
+ term = TERM
220
+ @body.each do |chunk|
221
+ size = chunk.bytesize
222
+ next if size == 0
223
+
224
+ yield [size.to_s(16), term, chunk.b, term].join
225
+ end
226
+ yield TAIL
227
+ yield term
228
+ end
229
+
230
+ # Close the response body if the response body supports it.
231
+ def close
232
+ @body.close if @body.respond_to?(:close)
233
+ end
234
+ end
235
+
196
236
  private
197
237
  # Set proper cache control and transfer encoding when streaming
198
238
  def _process_options(options)
@@ -211,7 +251,7 @@ module ActionController # :nodoc:
211
251
  # Call render_body if we are streaming instead of usual +render+.
212
252
  def _render_template(options)
213
253
  if options.delete(:stream)
214
- Rack::Chunked::Body.new view_renderer.render_body(view_context, options)
254
+ Body.new view_renderer.render_body(view_context, options)
215
255
  else
216
256
  super
217
257
  end