actionpack 7.2.2.1 → 8.1.2

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.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +408 -95
  3. data/README.rdoc +1 -1
  4. data/lib/abstract_controller/asset_paths.rb +4 -2
  5. data/lib/abstract_controller/base.rb +12 -17
  6. data/lib/abstract_controller/caching.rb +6 -3
  7. data/lib/abstract_controller/callbacks.rb +6 -0
  8. data/lib/abstract_controller/collector.rb +1 -1
  9. data/lib/abstract_controller/helpers.rb +1 -1
  10. data/lib/abstract_controller/logger.rb +2 -1
  11. data/lib/abstract_controller/rendering.rb +0 -1
  12. data/lib/action_controller/api.rb +1 -0
  13. data/lib/action_controller/base.rb +3 -2
  14. data/lib/action_controller/caching.rb +1 -2
  15. data/lib/action_controller/form_builder.rb +4 -4
  16. data/lib/action_controller/log_subscriber.rb +22 -3
  17. data/lib/action_controller/metal/allow_browser.rb +12 -2
  18. data/lib/action_controller/metal/conditional_get.rb +30 -1
  19. data/lib/action_controller/metal/data_streaming.rb +5 -5
  20. data/lib/action_controller/metal/exceptions.rb +5 -0
  21. data/lib/action_controller/metal/flash.rb +1 -4
  22. data/lib/action_controller/metal/head.rb +3 -1
  23. data/lib/action_controller/metal/instrumentation.rb +1 -2
  24. data/lib/action_controller/metal/live.rb +66 -26
  25. data/lib/action_controller/metal/params_wrapper.rb +3 -3
  26. data/lib/action_controller/metal/permissions_policy.rb +9 -0
  27. data/lib/action_controller/metal/rate_limiting.rb +39 -9
  28. data/lib/action_controller/metal/redirecting.rb +109 -16
  29. data/lib/action_controller/metal/renderers.rb +29 -9
  30. data/lib/action_controller/metal/rendering.rb +8 -2
  31. data/lib/action_controller/metal/request_forgery_protection.rb +21 -11
  32. data/lib/action_controller/metal/rescue.rb +9 -0
  33. data/lib/action_controller/metal/streaming.rb +5 -84
  34. data/lib/action_controller/metal/strong_parameters.rb +277 -92
  35. data/lib/action_controller/railtie.rb +33 -15
  36. data/lib/action_controller/renderer.rb +0 -1
  37. data/lib/action_controller/structured_event_subscriber.rb +116 -0
  38. data/lib/action_controller/test_case.rb +12 -2
  39. data/lib/action_dispatch/constants.rb +6 -0
  40. data/lib/action_dispatch/http/cache.rb +138 -11
  41. data/lib/action_dispatch/http/content_security_policy.rb +14 -1
  42. data/lib/action_dispatch/http/filter_parameters.rb +5 -3
  43. data/lib/action_dispatch/http/mime_negotiation.rb +63 -4
  44. data/lib/action_dispatch/http/mime_types.rb +1 -0
  45. data/lib/action_dispatch/http/param_builder.rb +187 -0
  46. data/lib/action_dispatch/http/param_error.rb +26 -0
  47. data/lib/action_dispatch/http/parameters.rb +3 -3
  48. data/lib/action_dispatch/http/permissions_policy.rb +6 -0
  49. data/lib/action_dispatch/http/query_parser.rb +55 -0
  50. data/lib/action_dispatch/http/request.rb +73 -23
  51. data/lib/action_dispatch/http/response.rb +65 -17
  52. data/lib/action_dispatch/http/url.rb +112 -16
  53. data/lib/action_dispatch/journey/formatter.rb +8 -3
  54. data/lib/action_dispatch/journey/gtg/simulator.rb +33 -12
  55. data/lib/action_dispatch/journey/gtg/transition_table.rb +37 -45
  56. data/lib/action_dispatch/journey/nodes/node.rb +2 -1
  57. data/lib/action_dispatch/journey/parser.rb +99 -196
  58. data/lib/action_dispatch/journey/route.rb +45 -31
  59. data/lib/action_dispatch/journey/router/utils.rb +8 -14
  60. data/lib/action_dispatch/journey/router.rb +59 -81
  61. data/lib/action_dispatch/journey/routes.rb +7 -0
  62. data/lib/action_dispatch/journey/scanner.rb +44 -42
  63. data/lib/action_dispatch/journey/visitors.rb +55 -23
  64. data/lib/action_dispatch/journey/visualizer/fsm.js +4 -6
  65. data/lib/action_dispatch/log_subscriber.rb +7 -3
  66. data/lib/action_dispatch/middleware/cookies.rb +8 -4
  67. data/lib/action_dispatch/middleware/debug_exceptions.rb +26 -5
  68. data/lib/action_dispatch/middleware/debug_view.rb +11 -5
  69. data/lib/action_dispatch/middleware/exception_wrapper.rb +14 -14
  70. data/lib/action_dispatch/middleware/executor.rb +17 -4
  71. data/lib/action_dispatch/middleware/public_exceptions.rb +6 -6
  72. data/lib/action_dispatch/middleware/remote_ip.rb +11 -5
  73. data/lib/action_dispatch/middleware/request_id.rb +2 -1
  74. data/lib/action_dispatch/middleware/session/cache_store.rb +17 -0
  75. data/lib/action_dispatch/middleware/ssl.rb +13 -3
  76. data/lib/action_dispatch/middleware/templates/rescues/_copy_button.html.erb +1 -0
  77. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +3 -5
  78. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +9 -5
  79. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +1 -0
  80. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +1 -0
  81. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +4 -0
  82. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +3 -0
  83. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +50 -0
  84. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +1 -0
  85. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +1 -0
  86. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -0
  87. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -0
  88. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -0
  89. data/lib/action_dispatch/railtie.rb +21 -0
  90. data/lib/action_dispatch/request/session.rb +1 -0
  91. data/lib/action_dispatch/request/utils.rb +9 -3
  92. data/lib/action_dispatch/routing/inspector.rb +80 -57
  93. data/lib/action_dispatch/routing/mapper.rb +409 -228
  94. data/lib/action_dispatch/routing/polymorphic_routes.rb +2 -2
  95. data/lib/action_dispatch/routing/redirection.rb +10 -7
  96. data/lib/action_dispatch/routing/route_set.rb +21 -12
  97. data/lib/action_dispatch/routing/routes_proxy.rb +1 -0
  98. data/lib/action_dispatch/structured_event_subscriber.rb +20 -0
  99. data/lib/action_dispatch/system_test_case.rb +3 -3
  100. data/lib/action_dispatch/system_testing/browser.rb +12 -21
  101. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +2 -2
  102. data/lib/action_dispatch/testing/assertion_response.rb +1 -1
  103. data/lib/action_dispatch/testing/assertions/response.rb +26 -2
  104. data/lib/action_dispatch/testing/assertions/routing.rb +27 -15
  105. data/lib/action_dispatch/testing/integration.rb +16 -7
  106. data/lib/action_dispatch/testing/request_encoder.rb +9 -9
  107. data/lib/action_dispatch/testing/test_process.rb +1 -2
  108. data/lib/action_dispatch.rb +14 -4
  109. data/lib/action_pack/gem_version.rb +3 -3
  110. metadata +19 -38
  111. data/lib/action_dispatch/journey/parser.y +0 -50
  112. data/lib/action_dispatch/journey/parser_extras.rb +0 -33
@@ -3,7 +3,6 @@
3
3
  # :markup: markdown
4
4
 
5
5
  require "abstract_controller/error"
6
- require "active_support/configurable"
7
6
  require "active_support/descendants_tracker"
8
7
  require "active_support/core_ext/module/anonymous"
9
8
  require "active_support/core_ext/module/attr_internal"
@@ -47,7 +46,7 @@ module AbstractController
47
46
  # Returns the formats that can be processed by the controller.
48
47
  attr_internal :formats
49
48
 
50
- include ActiveSupport::Configurable
49
+ class_attribute :config, instance_predicate: false, default: ActiveSupport::OrderedOptions.new
51
50
  extend ActiveSupport::DescendantsTracker
52
51
 
53
52
  class << self
@@ -65,6 +64,7 @@ module AbstractController
65
64
  unless klass.instance_variable_defined?(:@abstract)
66
65
  klass.instance_variable_set(:@abstract, false)
67
66
  end
67
+ klass.config = ActiveSupport::InheritableOptions.new(config)
68
68
  super
69
69
  end
70
70
 
@@ -86,22 +86,16 @@ module AbstractController
86
86
  controller.public_instance_methods(true) - methods
87
87
  end
88
88
 
89
- # A list of method names that should be considered actions. This includes all
89
+ # A `Set` of method names that should be considered actions. This includes all
90
90
  # public instance methods on a controller, less any internal methods (see
91
91
  # internal_methods), adding back in any methods that are internal, but still
92
92
  # exist on the class itself.
93
- #
94
- # #### Returns
95
- # * `Set` - A set of all methods that should be considered actions.
96
- #
97
93
  def action_methods
98
94
  @action_methods ||= begin
99
95
  # All public instance methods of this class, including ancestors except for
100
96
  # public instance methods of Base and its ancestors.
101
97
  methods = public_instance_methods(true) - internal_methods
102
- # Be sure to include shadowed public instance methods of this class.
103
- methods.concat(public_instance_methods(false))
104
- methods.map!(&:to_s)
98
+ methods.map!(&:name)
105
99
  methods.to_set
106
100
  end
107
101
  end
@@ -121,13 +115,14 @@ module AbstractController
121
115
  #
122
116
  # MyApp::MyPostsController.controller_path # => "my_app/my_posts"
123
117
  #
124
- # #### Returns
125
- # * `String`
126
- #
127
118
  def controller_path
128
119
  @controller_path ||= name.delete_suffix("Controller").underscore unless anonymous?
129
120
  end
130
121
 
122
+ def configure # :nodoc:
123
+ yield config
124
+ end
125
+
131
126
  # Refresh the cached action_methods when a new action_method is added.
132
127
  def method_added(name)
133
128
  super
@@ -147,10 +142,6 @@ module AbstractController
147
142
  # The actual method that is called is determined by calling #method_for_action.
148
143
  # If no method can handle the action, then an AbstractController::ActionNotFound
149
144
  # error is raised.
150
- #
151
- # #### Returns
152
- # * `self`
153
- #
154
145
  def process(action, ...)
155
146
  @_action_name = action.to_s
156
147
 
@@ -201,6 +192,10 @@ module AbstractController
201
192
  true
202
193
  end
203
194
 
195
+ def config # :nodoc:
196
+ @_config ||= self.class.config.inheritable_copy
197
+ end
198
+
204
199
  def inspect # :nodoc:
205
200
  "#<#{self.class.name}:#{'%#016x' % (object_id << 1)}>"
206
201
  end
@@ -32,13 +32,16 @@ module AbstractController
32
32
  included do
33
33
  extend ConfigMethods
34
34
 
35
- config_accessor :default_static_extension
35
+ singleton_class.delegate :default_static_extension, :default_static_extension=, to: :config
36
+ delegate :default_static_extension, :default_static_extension=, to: :config
36
37
  self.default_static_extension ||= ".html"
37
38
 
38
- config_accessor :perform_caching
39
+ singleton_class.delegate :perform_caching, :perform_caching=, to: :config
40
+ delegate :perform_caching, :perform_caching=, to: :config
39
41
  self.perform_caching = true if perform_caching.nil?
40
42
 
41
- config_accessor :enable_fragment_cache_logging
43
+ singleton_class.delegate :enable_fragment_cache_logging, :enable_fragment_cache_logging=, to: :config
44
+ delegate :enable_fragment_cache_logging, :enable_fragment_cache_logging=, to: :config
42
45
  self.enable_fragment_cache_logging = false
43
46
 
44
47
  class_attribute :_view_cache_dependencies, default: []
@@ -29,6 +29,8 @@ module AbstractController
29
29
  # ActiveSupport::Callbacks.
30
30
  include ActiveSupport::Callbacks
31
31
 
32
+ DEFAULT_INTERNAL_METHODS = [:_run_process_action_callbacks].freeze # :nodoc:
33
+
32
34
  included do
33
35
  define_callbacks :process_action,
34
36
  terminator: ->(controller, result_lambda) { result_lambda.call; controller.performed? },
@@ -251,6 +253,10 @@ module AbstractController
251
253
  # *_action is the same as append_*_action
252
254
  alias_method :"append_#{callback}_action", :"#{callback}_action"
253
255
  end
256
+
257
+ def internal_methods # :nodoc:
258
+ super.concat(DEFAULT_INTERNAL_METHODS)
259
+ end
254
260
  end
255
261
 
256
262
  private
@@ -27,7 +27,7 @@ module AbstractController
27
27
  def method_missing(symbol, ...)
28
28
  unless mime_constant = Mime[symbol]
29
29
  raise NoMethodError, "To respond to a custom format, register it as a MIME type first: " \
30
- "https://guides.rubyonrails.org/action_controller_overview.html#restful-downloads. " \
30
+ "https://guides.rubyonrails.org/action_controller_advanced_topics.html#restful-downloads. " \
31
31
  "If you meant to respond to a variant like :tablet or :phone, not a custom format, " \
32
32
  "be sure to nest your variant response within a format response: " \
33
33
  "format.html { |html| html.tablet { ... } }"
@@ -90,7 +90,7 @@ module AbstractController
90
90
  #--
91
91
  # Implemented by Resolution#modules_for_helpers.
92
92
 
93
- # :method: # all_helpers_from_path
93
+ # :method: all_helpers_from_path
94
94
  # :call-seq: all_helpers_from_path(path)
95
95
  #
96
96
  # Returns a list of helper names in a given path.
@@ -9,7 +9,8 @@ module AbstractController
9
9
  extend ActiveSupport::Concern
10
10
 
11
11
  included do
12
- config_accessor :logger
12
+ singleton_class.delegate :logger, :logger=, to: :config
13
+ delegate :logger, :logger=, to: :config
13
14
  include ActiveSupport::Benchmarkable
14
15
  end
15
16
  end
@@ -5,7 +5,6 @@
5
5
  require "abstract_controller/error"
6
6
  require "action_view"
7
7
  require "action_view/view_paths"
8
- require "set"
9
8
 
10
9
  module AbstractController
11
10
  class DoubleRenderError < Error
@@ -5,6 +5,7 @@
5
5
  require "action_view"
6
6
  require "action_controller"
7
7
  require "action_controller/log_subscriber"
8
+ require "action_controller/structured_event_subscriber"
8
9
 
9
10
  module ActionController
10
11
  # # Action Controller API
@@ -4,6 +4,7 @@
4
4
 
5
5
  require "action_view"
6
6
  require "action_controller/log_subscriber"
7
+ require "action_controller/structured_event_subscriber"
7
8
  require "action_controller/metal/params_wrapper"
8
9
 
9
10
  module ActionController
@@ -128,7 +129,7 @@ module ActionController
128
129
  #
129
130
  # Action Controller sends content to the user by using one of five rendering
130
131
  # methods. The most versatile and common is the rendering of a template.
131
- # Included in the Action Pack is the Action View, which enables rendering of ERB
132
+ # Also included with \Rails is Action View, which enables rendering of ERB
132
133
  # templates. It's automatically configured. The controller passes objects to the
133
134
  # view by assigning instance variables:
134
135
  #
@@ -266,7 +267,7 @@ module ActionController
266
267
  ParamsWrapper
267
268
  ]
268
269
 
269
- # Note: Documenting these severely degrates the performance of rdoc
270
+ # Note: Documenting these severely degrades the performance of rdoc
270
271
  # :stopdoc:
271
272
  include AbstractController::Rendering
272
273
  include AbstractController::Translation
@@ -9,9 +9,8 @@ module ActionController
9
9
  # of calculations, renderings, and database calls around for subsequent
10
10
  # requests.
11
11
  #
12
- # You can read more about each approach by clicking the modules below.
13
- #
14
12
  # Note: To turn off all caching provided by Action Controller, set
13
+ #
15
14
  # config.action_controller.perform_caching = false
16
15
  #
17
16
  # ## Caching stores
@@ -22,10 +22,10 @@ module ActionController
22
22
  # default_form_builder AdminFormBuilder
23
23
  # end
24
24
  #
25
- # Then in the view any form using `form_for` will be an instance of the
26
- # specified form builder:
25
+ # Then in the view any form using `form_with` or `form_for` will be an
26
+ # instance of the specified form builder:
27
27
  #
28
- # <%= form_for(@instance) do |builder| %>
28
+ # <%= form_with(model: @instance) do |builder| %>
29
29
  # <%= builder.special_field(:name) %>
30
30
  # <% end %>
31
31
  module FormBuilder
@@ -40,7 +40,7 @@ module ActionController
40
40
  # rendered by this controller and its subclasses.
41
41
  #
42
42
  # #### Parameters
43
- # * `builder` - Default form builder, an instance of
43
+ # * `builder` - Default form builder. Accepts a subclass of
44
44
  # ActionView::Helpers::FormBuilder
45
45
  def default_form_builder(builder)
46
46
  self._default_form_builder = builder
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # :markup: markdown
4
-
5
3
  module ActionController
6
- class LogSubscriber < ActiveSupport::LogSubscriber
4
+ class LogSubscriber < ActiveSupport::LogSubscriber # :nodoc:
7
5
  INTERNAL_PARAMS = %w(controller action format _method only_path)
8
6
 
7
+ class_attribute :backtrace_cleaner, default: ActiveSupport::BacktraceCleaner.new
8
+
9
9
  def start_processing(event)
10
10
  return unless logger.info?
11
11
 
@@ -49,6 +49,17 @@ module ActionController
49
49
  end
50
50
  subscribe_log_level :halted_callback, :info
51
51
 
52
+ # Manually subscribed below
53
+ def rescue_from_callback(event)
54
+ exception = event.payload[:exception]
55
+
56
+ exception_backtrace = exception.backtrace&.first
57
+ exception_backtrace = exception_backtrace&.delete_prefix("#{Rails.root}/") if defined?(Rails.root) && Rails.root
58
+
59
+ info { "rescue_from handled #{exception.class} (#{exception.message}) - #{exception_backtrace}" }
60
+ end
61
+ subscribe_log_level :rescue_from_callback, :info
62
+
52
63
  def send_file(event)
53
64
  info { "Sent file #{event.payload[:path]} (#{event.duration.round(1)}ms)" }
54
65
  end
@@ -56,6 +67,10 @@ module ActionController
56
67
 
57
68
  def redirect_to(event)
58
69
  info { "Redirected to #{event.payload[:location]}" }
70
+
71
+ if ActionDispatch.verbose_redirect_logs && (source = redirect_source_location)
72
+ info { "↳ #{source}" }
73
+ end
59
74
  end
60
75
  subscribe_log_level :redirect_to, :info
61
76
 
@@ -90,6 +105,10 @@ module ActionController
90
105
  def logger
91
106
  ActionController::Base.logger
92
107
  end
108
+
109
+ def redirect_source_location
110
+ backtrace_cleaner.first_clean_frame
111
+ end
93
112
  end
94
113
  end
95
114
 
@@ -14,7 +14,7 @@ module ActionController # :nodoc:
14
14
  # aren't reporting a user-agent header, will be allowed access.
15
15
  #
16
16
  # A browser that's blocked will by default be served the file in
17
- # public/406-unsupported-browser.html with a HTTP status code of "406 Not
17
+ # public/406-unsupported-browser.html with an HTTP status code of "406 Not
18
18
  # Acceptable".
19
19
  #
20
20
  # In addition to specifically named browser versions, you can also pass
@@ -36,6 +36,16 @@ module ActionController # :nodoc:
36
36
  # end
37
37
  #
38
38
  # class ApplicationController < ActionController::Base
39
+ # # Allow only browsers natively supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has
40
+ # allow_browser versions: :modern, block: :handle_outdated_browser
41
+ #
42
+ # private
43
+ # def handle_outdated_browser
44
+ # render file: Rails.root.join("public/custom-error.html"), status: :not_acceptable
45
+ # end
46
+ # end
47
+ #
48
+ # class ApplicationController < ActionController::Base
39
49
  # # All versions of Chrome and Opera will be allowed, but no versions of "internet explorer" (ie). Safari needs to be 16.4+ and Firefox 121+.
40
50
  # allow_browser versions: { safari: 16.4, firefox: 121, ie: false }
41
51
  # end
@@ -55,7 +65,7 @@ module ActionController # :nodoc:
55
65
 
56
66
  if BrowserBlocker.new(request, versions: versions).blocked?
57
67
  ActiveSupport::Notifications.instrument("browser_block.action_controller", request: request, versions: versions) do
58
- instance_exec(&block)
68
+ block.is_a?(Symbol) ? send(block) : instance_exec(&block)
59
69
  end
60
70
  end
61
71
  end
@@ -259,6 +259,9 @@ module ActionController
259
259
  # `:stale_if_error`
260
260
  # : Sets the value of the `stale-if-error` directive.
261
261
  #
262
+ # `:immutable`
263
+ # : If true, adds the `immutable` directive.
264
+ #
262
265
  #
263
266
  # Any additional key-value pairs are concatenated as directives. For a list of
264
267
  # supported `Cache-Control` directives, see the [article on
@@ -292,6 +295,7 @@ module ActionController
292
295
  must_revalidate: options.delete(:must_revalidate),
293
296
  stale_while_revalidate: options.delete(:stale_while_revalidate),
294
297
  stale_if_error: options.delete(:stale_if_error),
298
+ immutable: options.delete(:immutable),
295
299
  )
296
300
  options.delete(:private)
297
301
 
@@ -315,7 +319,7 @@ module ActionController
315
319
  # user's web browser. To allow proxies to cache the response, set `true` to
316
320
  # indicate that they can serve the cached response to all users.
317
321
  def http_cache_forever(public: false)
318
- expires_in 100.years, public: public
322
+ expires_in 100.years, public: public, immutable: true
319
323
 
320
324
  yield if stale?(etag: request.fullpath,
321
325
  last_modified: Time.new(2011, 1, 1).utc,
@@ -328,6 +332,31 @@ module ActionController
328
332
  response.cache_control.replace(no_store: true)
329
333
  end
330
334
 
335
+ # Adds the `must-understand` directive to the `Cache-Control` header, which indicates
336
+ # that a cache MUST understand the semantics of the response status code that has been
337
+ # received, or discard the response.
338
+ #
339
+ # This is particularly useful when returning responses with new or uncommon
340
+ # status codes that might not be properly interpreted by older caches.
341
+ #
342
+ # #### Example
343
+ #
344
+ # def show
345
+ # @article = Article.find(params[:id])
346
+ #
347
+ # if @article.early_access?
348
+ # must_understand
349
+ # render status: 203 # Non-Authoritative Information
350
+ # else
351
+ # fresh_when @article
352
+ # end
353
+ # end
354
+ #
355
+ def must_understand
356
+ response.cache_control[:must_understand] = true
357
+ response.cache_control[:no_store] = true
358
+ end
359
+
331
360
  private
332
361
  def combine_etags(validator, options)
333
362
  [validator, *etaggers.map { |etagger| instance_exec(options, &etagger) }].compact
@@ -28,7 +28,8 @@ module ActionController # :nodoc:
28
28
  # `send_file(params[:path])` allows a malicious user to download any file on
29
29
  # your server.
30
30
  #
31
- # Options:
31
+ # #### Options:
32
+ #
32
33
  # * `:filename` - suggests a filename for the browser to use. Defaults to
33
34
  # `File.basename(path)`.
34
35
  # * `:type` - specifies an HTTP content type. You can specify either a string
@@ -90,7 +91,8 @@ module ActionController # :nodoc:
90
91
  # inline data. You may also set the content type, the file name, and other
91
92
  # things.
92
93
  #
93
- # Options:
94
+ # #### Options:
95
+ #
94
96
  # * `:filename` - suggests a filename for the browser to use.
95
97
  # * `:type` - specifies an HTTP content type. Defaults to
96
98
  # `application/octet-stream`. You can specify either a string or a symbol
@@ -132,9 +134,7 @@ module ActionController # :nodoc:
132
134
  raise ArgumentError, ":type option required" if content_type.nil?
133
135
 
134
136
  if content_type.is_a?(Symbol)
135
- extension = Mime[content_type]
136
- raise ArgumentError, "Unknown MIME type #{options[:type]}" unless extension
137
- self.content_type = extension
137
+ self.content_type = content_type
138
138
  else
139
139
  if !type_provided && options[:filename]
140
140
  # If type wasn't provided, try guessing from file extension.
@@ -103,4 +103,9 @@ module ActionController
103
103
  super(message)
104
104
  end
105
105
  end
106
+
107
+ # Raised when a Rate Limit is exceeded by too many requests within a period of
108
+ # time.
109
+ class TooManyRequests < ActionControllerError
110
+ end
106
111
  end
@@ -38,15 +38,12 @@ module ActionController # :nodoc:
38
38
  define_method(type) do
39
39
  request.flash[type]
40
40
  end
41
+ private type
41
42
  helper_method(type) if respond_to?(:helper_method)
42
43
 
43
44
  self._flash_types += [type]
44
45
  end
45
46
  end
46
-
47
- def action_methods # :nodoc:
48
- @action_methods ||= super - _flash_types.map(&:to_s).to_set
49
- end
50
47
  end
51
48
 
52
49
  private
@@ -25,6 +25,8 @@ module ActionController
25
25
  raise ArgumentError, "#{status.inspect} is not a valid value for `status`."
26
26
  end
27
27
 
28
+ raise ::AbstractController::DoubleRenderError if response_body
29
+
28
30
  status ||= :ok
29
31
 
30
32
  if options
@@ -41,7 +43,7 @@ module ActionController
41
43
 
42
44
  if include_content?(response_code)
43
45
  unless self.media_type
44
- self.content_type = content_type || ((f = formats) && Mime[f.first]) || Mime[:html]
46
+ self.content_type = content_type || ((f = formats) && Mime[f.first]) || :html
45
47
  end
46
48
 
47
49
  response.charset = false
@@ -2,7 +2,6 @@
2
2
 
3
3
  # :markup: markdown
4
4
 
5
- require "benchmark"
6
5
  require "abstract_controller/logger"
7
6
 
8
7
  module ActionController
@@ -29,7 +28,7 @@ module ActionController
29
28
  def render(*)
30
29
  render_output = nil
31
30
  self.view_runtime = cleanup_view_runtime do
32
- Benchmark.ms { render_output = super }
31
+ ActiveSupport::Benchmark.realtime(:float_millisecond) { render_output = super }
33
32
  end
34
33
  render_output
35
34
  end
@@ -53,12 +53,53 @@ module ActionController
53
53
  # response.headers["Last-Modified"] = Time.now.httpdate # Add this line if your Rack version is 2.2.x
54
54
  # ...
55
55
  # end
56
+ #
57
+ # ## Streaming and Execution State
58
+ #
59
+ # When streaming, the action is executed in a separate thread. By default, this thread
60
+ # shares execution state from the parent thread.
61
+ #
62
+ # You can configure which execution state keys should be excluded from being shared
63
+ # using the `config.action_controller.live_streaming_excluded_keys` configuration:
64
+ #
65
+ # # config/application.rb
66
+ # config.action_controller.live_streaming_excluded_keys = [:active_record_connected_to_stack]
67
+ #
68
+ # This is useful when using ActionController::Live inside a `connected_to` block. For example,
69
+ # if the parent request is reading from a replica using `connected_to(role: :reading)`, you may
70
+ # want the streaming thread to use its own connection context instead of inheriting the read-only
71
+ # context:
72
+ #
73
+ # # Without configuration, streaming thread inherits read-only connection
74
+ # ActiveRecord::Base.connected_to(role: :reading) do
75
+ # @posts = Post.all
76
+ # render stream: true # Streaming thread cannot write to database
77
+ # end
78
+ #
79
+ # # With configuration, streaming thread gets fresh connection context
80
+ # # config.action_controller.live_streaming_excluded_keys = [:active_record_connected_to_stack]
81
+ # ActiveRecord::Base.connected_to(role: :reading) do
82
+ # @posts = Post.all
83
+ # render stream: true # Streaming thread can write to database if needed
84
+ # end
85
+ #
86
+ # Common keys you might want to exclude:
87
+ # - `:active_record_connected_to_stack` - Database connection routing and roles
88
+ # - `:active_record_prohibit_shard_swapping` - Shard swapping restrictions
89
+ #
90
+ # By default, no keys are excluded to maintain backward compatibility.
56
91
  module Live
57
92
  extend ActiveSupport::Concern
58
93
 
94
+ mattr_accessor :live_streaming_excluded_keys, default: []
95
+
96
+ included do
97
+ class_attribute :live_streaming_excluded_keys, instance_accessor: false, default: Live.live_streaming_excluded_keys
98
+ end
99
+
59
100
  module ClassMethods
60
101
  def make_response!(request)
61
- if request.get_header("HTTP_VERSION") == "HTTP/1.0"
102
+ if (request.get_header("SERVER_PROTOCOL") || request.get_header("HTTP_VERSION")) == "HTTP/1.0"
62
103
  super
63
104
  else
64
105
  Live::Response.new.tap do |res|
@@ -133,15 +174,16 @@ module ActionController
133
174
  private
134
175
  def perform_write(json, options)
135
176
  current_options = @options.merge(options).stringify_keys
136
-
177
+ event = +""
137
178
  PERMITTED_OPTIONS.each do |option_name|
138
179
  if (option_value = current_options[option_name])
139
- @stream.write "#{option_name}: #{option_value}\n"
180
+ event << "#{option_name}: #{option_value}\n"
140
181
  end
141
182
  end
142
183
 
143
184
  message = json.gsub("\n", "\ndata: ")
144
- @stream.write "data: #{message}\n\n"
185
+ event << "data: #{message}\n\n"
186
+ @stream.write event
145
187
  end
146
188
  end
147
189
 
@@ -171,12 +213,6 @@ module ActionController
171
213
  @ignore_disconnect = false
172
214
  end
173
215
 
174
- # ActionDispatch::Response delegates #to_ary to the internal
175
- # ActionDispatch::Response::Buffer, defining #to_ary is an indicator that the
176
- # response body can be buffered and/or cached by Rack middlewares, this is not
177
- # the case for Live responses so we undefine it for this Buffer subclass.
178
- undef_method :to_ary
179
-
180
216
  def write(string)
181
217
  unless @response.committed?
182
218
  @response.headers["Cache-Control"] ||= "no-cache"
@@ -242,12 +278,7 @@ module ActionController
242
278
 
243
279
  private
244
280
  def each_chunk(&block)
245
- loop do
246
- str = nil
247
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
248
- str = @buf.pop
249
- end
250
- break unless str
281
+ while str = @buf.pop
251
282
  yield str
252
283
  end
253
284
  end
@@ -281,16 +312,15 @@ module ActionController
281
312
  # This processes the action in a child thread. It lets us return the response
282
313
  # code and headers back up the Rack stack, and still process the body in
283
314
  # parallel with sending data to the client.
284
- new_controller_thread {
315
+ new_controller_thread do
285
316
  ActiveSupport::Dependencies.interlock.running do
286
317
  t2 = Thread.current
287
318
 
288
319
  # Since we're processing the view in a different thread, copy the thread locals
289
320
  # from the main thread to the child thread. :'(
290
321
  locals.each { |k, v| t2[k] = v }
291
- ActiveSupport::IsolatedExecutionState.share_with(t1)
292
322
 
293
- begin
323
+ ActiveSupport::IsolatedExecutionState.share_with(t1, except: self.class.live_streaming_excluded_keys) do
294
324
  super(name)
295
325
  rescue => e
296
326
  if @_response.committed?
@@ -307,15 +337,15 @@ module ActionController
307
337
  error = e
308
338
  end
309
339
  ensure
340
+ clean_up_thread_locals(locals, t2)
341
+
310
342
  @_response.commit!
311
343
  end
312
344
  end
313
- }
314
-
315
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
316
- @_response.await_commit
317
345
  end
318
346
 
347
+ @_response.await_commit
348
+
319
349
  raise error if error
320
350
  end
321
351
 
@@ -328,7 +358,8 @@ module ActionController
328
358
  # or other running data where you don't want the entire file buffered in memory
329
359
  # first. Similar to send_data, but where the data is generated live.
330
360
  #
331
- # Options:
361
+ # #### Options:
362
+ #
332
363
  # * `:filename` - suggests a filename for the browser to use.
333
364
  # * `:type` - specifies an HTTP content type. You can specify either a string
334
365
  # or a symbol for a registered type with `Mime::Type.register`, for example
@@ -371,11 +402,20 @@ module ActionController
371
402
  # data from the response bodies. Nobody should call this method except in Rails
372
403
  # internals. Seriously!
373
404
  def new_controller_thread # :nodoc:
374
- Thread.new {
405
+ ActionController::Live.live_thread_pool_executor.post do
375
406
  t2 = Thread.current
376
407
  t2.abort_on_exception = true
377
408
  yield
378
- }
409
+ end
410
+ end
411
+
412
+ # Ensure we clean up any thread locals we copied so that the thread can reused.
413
+ def clean_up_thread_locals(locals, thread) # :nodoc:
414
+ locals.each { |k, _| thread[k] = nil }
415
+ end
416
+
417
+ def self.live_thread_pool_executor
418
+ @live_thread_pool_executor ||= Concurrent::CachedThreadPool.new(name: "action_controller.live")
379
419
  end
380
420
 
381
421
  def log_error(exception)
@@ -198,14 +198,14 @@ module ActionController
198
198
  # # enables the parameter wrapper for XML format
199
199
  #
200
200
  # wrap_parameters :person
201
- # # wraps parameters into +params[:person]+ hash
201
+ # # wraps parameters into params[:person] hash
202
202
  #
203
203
  # wrap_parameters Person
204
204
  # # wraps parameters by determining the wrapper key from Person class
205
- # # (+person+, in this case) and the list of attribute names
205
+ # # (:person, in this case) and the list of attribute names
206
206
  #
207
207
  # wrap_parameters include: [:username, :title]
208
- # # wraps only +:username+ and +:title+ attributes from parameters.
208
+ # # wraps only :username and :title attributes from parameters.
209
209
  #
210
210
  # wrap_parameters false
211
211
  # # disables parameters wrapping for this controller altogether.