actionpack 7.0.5 → 7.1.3.4

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


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

Files changed (138) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +368 -315
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +4 -4
  5. data/lib/abstract_controller/base.rb +20 -11
  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 +68 -22
  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 +9 -6
  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 +17 -8
  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 +24 -7
  37. data/lib/action_controller/metal/renderers.rb +4 -4
  38. data/lib/action_controller/metal/rendering.rb +0 -7
  39. data/lib/action_controller/metal/request_forgery_protection.rb +139 -50
  40. data/lib/action_controller/metal/rescue.rb +6 -3
  41. data/lib/action_controller/metal/streaming.rb +70 -30
  42. data/lib/action_controller/metal/strong_parameters.rb +158 -101
  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 +9 -8
  53. data/lib/action_dispatch/http/filter_parameters.rb +11 -5
  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 +38 -23
  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 +80 -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 +9 -8
  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 +186 -27
  78. data/lib/action_dispatch/middleware/executor.rb +1 -1
  79. data/lib/action_dispatch/middleware/flash.rb +7 -0
  80. data/lib/action_dispatch/middleware/host_authorization.rb +18 -8
  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 +25 -18
  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/blocked_host.html.erb +7 -3
  98. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +5 -3
  99. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +7 -7
  100. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +2 -2
  101. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +17 -0
  102. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +16 -12
  103. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +1 -1
  104. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +3 -3
  105. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +4 -4
  106. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -1
  107. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +1 -1
  108. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +3 -0
  109. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +46 -37
  110. data/lib/action_dispatch/railtie.rb +14 -4
  111. data/lib/action_dispatch/request/session.rb +16 -6
  112. data/lib/action_dispatch/request/utils.rb +8 -3
  113. data/lib/action_dispatch/routing/inspector.rb +54 -6
  114. data/lib/action_dispatch/routing/mapper.rb +35 -24
  115. data/lib/action_dispatch/routing/polymorphic_routes.rb +2 -0
  116. data/lib/action_dispatch/routing/redirection.rb +15 -6
  117. data/lib/action_dispatch/routing/route_set.rb +52 -22
  118. data/lib/action_dispatch/routing/routes_proxy.rb +10 -15
  119. data/lib/action_dispatch/routing/url_for.rb +5 -1
  120. data/lib/action_dispatch/routing.rb +7 -7
  121. data/lib/action_dispatch/system_test_case.rb +3 -3
  122. data/lib/action_dispatch/system_testing/browser.rb +20 -19
  123. data/lib/action_dispatch/system_testing/driver.rb +13 -21
  124. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +27 -16
  125. data/lib/action_dispatch/testing/assertion_response.rb +1 -1
  126. data/lib/action_dispatch/testing/assertions/response.rb +13 -6
  127. data/lib/action_dispatch/testing/assertions/routing.rb +67 -28
  128. data/lib/action_dispatch/testing/assertions.rb +3 -1
  129. data/lib/action_dispatch/testing/integration.rb +27 -17
  130. data/lib/action_dispatch/testing/request_encoder.rb +4 -1
  131. data/lib/action_dispatch/testing/test_process.rb +4 -3
  132. data/lib/action_dispatch/testing/test_request.rb +1 -1
  133. data/lib/action_dispatch/testing/test_response.rb +23 -9
  134. data/lib/action_dispatch.rb +37 -4
  135. data/lib/action_pack/gem_version.rb +4 -4
  136. data/lib/action_pack/version.rb +1 -1
  137. data/lib/action_pack.rb +1 -1
  138. metadata +67 -31
@@ -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?
@@ -134,6 +129,7 @@ module ActionController # :nodoc:
134
129
  #
135
130
  # If you need to add verification to the beginning of the callback chain, use <tt>prepend: true</tt>.
136
131
  # * <tt>:with</tt> - Set the method to handle unverified request.
132
+ # Note if <tt>default_protect_from_forgery</tt> is true, Rails call protect_from_forgery with <tt>with :exception</tt>.
137
133
  #
138
134
  # Built-in unverified request handling methods are:
139
135
  # * <tt>:exception</tt> - Raises ActionController::InvalidAuthenticityToken exception.
@@ -148,18 +144,46 @@ module ActionController # :nodoc:
148
144
  # end
149
145
  #
150
146
  # def handle_unverified_request
151
- # # Custom behaviour for unverfied request
147
+ # # Custom behavior for unverfied request
152
148
  # end
153
149
  # end
154
150
  #
155
- # class ApplicationController < ActionController:x:Base
151
+ # class ApplicationController < ActionController::Base
156
152
  # protect_from_forgery with: CustomStrategy
157
153
  # end
154
+ # * <tt>:store</tt> - Set the strategy to store and retrieve CSRF tokens.
155
+ #
156
+ # Built-in session token strategies are:
157
+ # * <tt>:session</tt> - Store the CSRF token in the session. Used as default if <tt>:store</tt> option is not specified.
158
+ # * <tt>:cookie</tt> - Store the CSRF token in an encrypted cookie.
159
+ #
160
+ # You can also implement custom strategy classes for CSRF token storage:
161
+ #
162
+ # class CustomStore
163
+ # def fetch(request)
164
+ # # Return the token from a custom location
165
+ # end
166
+ #
167
+ # def store(request, csrf_token)
168
+ # # Store the token in a custom location
169
+ # end
170
+ #
171
+ # def reset(request)
172
+ # # Delete the stored session token
173
+ # end
174
+ # end
175
+ #
176
+ # class ApplicationController < ActionController::Base
177
+ # protect_from_forgery store: CustomStore.new
178
+ # end
158
179
  def protect_from_forgery(options = {})
159
180
  options = options.reverse_merge(prepend: false)
160
181
 
161
182
  self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session)
162
183
  self.request_forgery_protection_token ||= :authenticity_token
184
+
185
+ self.csrf_token_storage_strategy = storage_strategy(options[:store] || SessionStore.new)
186
+
163
187
  before_action :verify_authenticity_token, options
164
188
  append_after_action :verify_same_origin_request
165
189
  end
@@ -188,6 +212,22 @@ module ActionController # :nodoc:
188
212
  raise ArgumentError, "Invalid request forgery protection method, use :null_session, :exception, :reset_session, or a custom forgery protection class."
189
213
  end
190
214
  end
215
+
216
+ def storage_strategy(name)
217
+ case name
218
+ when :session
219
+ SessionStore.new
220
+ when :cookie
221
+ CookieStore.new(:csrf_token)
222
+ else
223
+ return name if is_storage_strategy?(name)
224
+ raise ArgumentError, "Invalid CSRF token storage strategy, use :session, :cookie, or a custom CSRF token storage class."
225
+ end
226
+ end
227
+
228
+ def is_storage_strategy?(object)
229
+ object.respond_to?(:fetch) && object.respond_to?(:store) && object.respond_to?(:reset)
230
+ end
191
231
  end
192
232
 
193
233
  module ProtectionMethods
@@ -255,6 +295,68 @@ module ActionController # :nodoc:
255
295
  end
256
296
  end
257
297
 
298
+ class SessionStore
299
+ def fetch(request)
300
+ request.session[:_csrf_token]
301
+ end
302
+
303
+ def store(request, csrf_token)
304
+ request.session[:_csrf_token] = csrf_token
305
+ end
306
+
307
+ def reset(request)
308
+ request.session.delete(:_csrf_token)
309
+ end
310
+ end
311
+
312
+ class CookieStore
313
+ def initialize(cookie = :csrf_token)
314
+ @cookie_name = cookie
315
+ end
316
+
317
+ def fetch(request)
318
+ contents = request.cookie_jar.encrypted[@cookie_name]
319
+ return nil if contents.nil?
320
+
321
+ value = JSON.parse(contents)
322
+ return nil unless value.dig("session_id", "public_id") == request.session.id_was&.public_id
323
+
324
+ value["token"]
325
+ rescue JSON::ParserError
326
+ nil
327
+ end
328
+
329
+ def store(request, csrf_token)
330
+ request.cookie_jar.encrypted.permanent[@cookie_name] = {
331
+ value: {
332
+ token: csrf_token,
333
+ session_id: request.session.id,
334
+ }.to_json,
335
+ httponly: true,
336
+ same_site: :lax,
337
+ }
338
+ end
339
+
340
+ def reset(request)
341
+ request.cookie_jar.delete(@cookie_name)
342
+ end
343
+ end
344
+
345
+ def initialize(...)
346
+ super
347
+ @marked_for_same_origin_verification = nil
348
+ end
349
+
350
+ def reset_csrf_token(request) # :doc:
351
+ request.env.delete(CSRF_TOKEN)
352
+ csrf_token_storage_strategy.reset(request)
353
+ end
354
+
355
+ def commit_csrf_token(request) # :doc:
356
+ csrf_token = request.env[CSRF_TOKEN]
357
+ csrf_token_storage_strategy.store(request, csrf_token) unless csrf_token.nil?
358
+ end
359
+
258
360
  private
259
361
  # The actual before_action that is used to verify the CSRF token.
260
362
  # Don't override this directly. Provide your own forgery protection
@@ -356,20 +458,20 @@ module ActionController # :nodoc:
356
458
 
357
459
  # Creates the authenticity token for the current request.
358
460
  def form_authenticity_token(form_options: {}) # :doc:
359
- masked_authenticity_token(session, form_options: form_options)
461
+ masked_authenticity_token(form_options: form_options)
360
462
  end
361
463
 
362
464
  # Creates a masked version of the authenticity token that varies
363
465
  # on each request. The masking is used to mitigate SSL attacks
364
466
  # like BREACH.
365
- def masked_authenticity_token(session, form_options: {})
467
+ def masked_authenticity_token(form_options: {})
366
468
  action, method = form_options.values_at(:action, :method)
367
469
 
368
470
  raw_token = if per_form_csrf_tokens && action && method
369
471
  action_path = normalize_action_path(action)
370
- per_form_csrf_token(session, action_path, method)
472
+ per_form_csrf_token(nil, action_path, method)
371
473
  else
372
- global_csrf_token(session)
474
+ global_csrf_token
373
475
  end
374
476
 
375
477
  mask_token(raw_token)
@@ -397,14 +499,14 @@ module ActionController # :nodoc:
397
499
  # This is actually an unmasked token. This is expected if
398
500
  # you have just upgraded to masked tokens, but should stop
399
501
  # happening shortly after installing this gem.
400
- compare_with_real_token masked_token, session
502
+ compare_with_real_token masked_token
401
503
 
402
504
  elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
403
505
  csrf_token = unmask_token(masked_token)
404
506
 
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)
507
+ compare_with_global_token(csrf_token) ||
508
+ compare_with_real_token(csrf_token) ||
509
+ valid_per_form_csrf_token?(csrf_token)
408
510
  else
409
511
  false # Token is malformed.
410
512
  end
@@ -425,15 +527,15 @@ module ActionController # :nodoc:
425
527
  encode_csrf_token(masked_token)
426
528
  end
427
529
 
428
- def compare_with_real_token(token, session) # :doc:
530
+ def compare_with_real_token(token, session = nil) # :doc:
429
531
  ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, real_csrf_token(session))
430
532
  end
431
533
 
432
- def compare_with_global_token(token, session) # :doc:
534
+ def compare_with_global_token(token, session = nil) # :doc:
433
535
  ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, global_csrf_token(session))
434
536
  end
435
537
 
436
- def valid_per_form_csrf_token?(token, session) # :doc:
538
+ def valid_per_form_csrf_token?(token, session = nil) # :doc:
437
539
  if per_form_csrf_tokens
438
540
  correct_token = per_form_csrf_token(
439
541
  session,
@@ -447,9 +549,12 @@ module ActionController # :nodoc:
447
549
  end
448
550
  end
449
551
 
450
- def real_csrf_token(session) # :doc:
451
- session[:_csrf_token] ||= generate_csrf_token
452
- decode_csrf_token(session[:_csrf_token])
552
+ def real_csrf_token(_session = nil) # :doc:
553
+ csrf_token = request.env.fetch(CSRF_TOKEN) do
554
+ request.env[CSRF_TOKEN] = csrf_token_storage_strategy.fetch(request) || generate_csrf_token
555
+ end
556
+
557
+ decode_csrf_token(csrf_token)
453
558
  end
454
559
 
455
560
  def per_form_csrf_token(session, action_path, method) # :doc:
@@ -459,7 +564,7 @@ module ActionController # :nodoc:
459
564
  GLOBAL_CSRF_TOKEN_IDENTIFIER = "!real_csrf_token"
460
565
  private_constant :GLOBAL_CSRF_TOKEN_IDENTIFIER
461
566
 
462
- def global_csrf_token(session) # :doc:
567
+ def global_csrf_token(session = nil) # :doc:
463
568
  csrf_token_hmac(session, GLOBAL_CSRF_TOKEN_IDENTIFIER)
464
569
  end
465
570
 
@@ -519,31 +624,15 @@ module ActionController # :nodoc:
519
624
  end
520
625
 
521
626
  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
627
+ SecureRandom.urlsafe_base64(AUTHENTICITY_TOKEN_LENGTH)
527
628
  end
528
629
 
529
630
  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
631
+ Base64.urlsafe_encode64(csrf_token, padding: false)
535
632
  end
536
633
 
537
634
  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
635
+ Base64.urlsafe_decode64(encoded_csrf_token)
547
636
  end
548
637
  end
549
638
  end
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionController # :nodoc:
4
- # This module is responsible for providing +rescue_from+ helpers
5
- # to controllers and configuring when detailed exceptions must be
6
- # shown.
4
+ # = Action Controller \Rescue
5
+ #
6
+ # This module is responsible for providing
7
+ # {rescue_from}[rdoc-ref:ActiveSupport::Rescuable::ClassMethods#rescue_from]
8
+ # to controllers, wrapping actions to handle configured errors, and
9
+ # configuring when detailed exceptions must be shown.
7
10
  module Rescue
8
11
  extend ActiveSupport::Concern
9
12
  include ActiveSupport::Rescuable
@@ -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