actionpack 7.1.5.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 (177) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +308 -523
  3. data/README.rdoc +1 -1
  4. data/lib/abstract_controller/asset_paths.rb +6 -2
  5. data/lib/abstract_controller/base.rb +104 -105
  6. data/lib/abstract_controller/caching/fragments.rb +50 -53
  7. data/lib/abstract_controller/caching.rb +8 -3
  8. data/lib/abstract_controller/callbacks.rb +70 -62
  9. data/lib/abstract_controller/collector.rb +7 -7
  10. data/lib/abstract_controller/deprecator.rb +2 -0
  11. data/lib/abstract_controller/error.rb +2 -0
  12. data/lib/abstract_controller/helpers.rb +71 -84
  13. data/lib/abstract_controller/logger.rb +4 -1
  14. data/lib/abstract_controller/railties/routes_helpers.rb +2 -0
  15. data/lib/abstract_controller/rendering.rb +13 -13
  16. data/lib/abstract_controller/translation.rb +12 -13
  17. data/lib/abstract_controller/url_for.rb +8 -6
  18. data/lib/abstract_controller.rb +2 -0
  19. data/lib/action_controller/api/api_rendering.rb +2 -0
  20. data/lib/action_controller/api.rb +76 -72
  21. data/lib/action_controller/base.rb +199 -126
  22. data/lib/action_controller/caching.rb +16 -14
  23. data/lib/action_controller/deprecator.rb +2 -0
  24. data/lib/action_controller/form_builder.rb +21 -18
  25. data/lib/action_controller/log_subscriber.rb +23 -2
  26. data/lib/action_controller/metal/allow_browser.rb +133 -0
  27. data/lib/action_controller/metal/basic_implicit_render.rb +2 -0
  28. data/lib/action_controller/metal/conditional_get.rb +217 -175
  29. data/lib/action_controller/metal/content_security_policy.rb +25 -24
  30. data/lib/action_controller/metal/cookies.rb +4 -2
  31. data/lib/action_controller/metal/data_streaming.rb +72 -63
  32. data/lib/action_controller/metal/default_headers.rb +5 -3
  33. data/lib/action_controller/metal/etag_with_flash.rb +3 -1
  34. data/lib/action_controller/metal/etag_with_template_digest.rb +17 -15
  35. data/lib/action_controller/metal/exceptions.rb +16 -9
  36. data/lib/action_controller/metal/flash.rb +13 -14
  37. data/lib/action_controller/metal/head.rb +15 -11
  38. data/lib/action_controller/metal/helpers.rb +63 -55
  39. data/lib/action_controller/metal/http_authentication.rb +209 -201
  40. data/lib/action_controller/metal/implicit_render.rb +17 -15
  41. data/lib/action_controller/metal/instrumentation.rb +16 -14
  42. data/lib/action_controller/metal/live.rb +177 -128
  43. data/lib/action_controller/metal/logging.rb +6 -4
  44. data/lib/action_controller/metal/mime_responds.rb +151 -142
  45. data/lib/action_controller/metal/parameter_encoding.rb +34 -32
  46. data/lib/action_controller/metal/params_wrapper.rb +57 -59
  47. data/lib/action_controller/metal/permissions_policy.rb +22 -12
  48. data/lib/action_controller/metal/rate_limiting.rb +92 -0
  49. data/lib/action_controller/metal/redirecting.rb +213 -94
  50. data/lib/action_controller/metal/renderers.rb +78 -57
  51. data/lib/action_controller/metal/rendering.rb +111 -77
  52. data/lib/action_controller/metal/request_forgery_protection.rb +182 -143
  53. data/lib/action_controller/metal/rescue.rb +20 -9
  54. data/lib/action_controller/metal/streaming.rb +118 -195
  55. data/lib/action_controller/metal/strong_parameters.rb +720 -530
  56. data/lib/action_controller/metal/testing.rb +2 -0
  57. data/lib/action_controller/metal/url_for.rb +17 -15
  58. data/lib/action_controller/metal.rb +86 -60
  59. data/lib/action_controller/railtie.rb +36 -15
  60. data/lib/action_controller/railties/helpers.rb +2 -0
  61. data/lib/action_controller/renderer.rb +41 -36
  62. data/lib/action_controller/structured_event_subscriber.rb +116 -0
  63. data/lib/action_controller/template_assertions.rb +4 -2
  64. data/lib/action_controller/test_case.rb +160 -131
  65. data/lib/action_controller.rb +5 -1
  66. data/lib/action_dispatch/constants.rb +8 -0
  67. data/lib/action_dispatch/deprecator.rb +2 -0
  68. data/lib/action_dispatch/http/cache.rb +163 -35
  69. data/lib/action_dispatch/http/content_disposition.rb +2 -0
  70. data/lib/action_dispatch/http/content_security_policy.rb +54 -39
  71. data/lib/action_dispatch/http/filter_parameters.rb +14 -8
  72. data/lib/action_dispatch/http/filter_redirect.rb +22 -1
  73. data/lib/action_dispatch/http/headers.rb +22 -22
  74. data/lib/action_dispatch/http/mime_negotiation.rb +89 -41
  75. data/lib/action_dispatch/http/mime_type.rb +25 -21
  76. data/lib/action_dispatch/http/mime_types.rb +3 -0
  77. data/lib/action_dispatch/http/param_builder.rb +187 -0
  78. data/lib/action_dispatch/http/param_error.rb +26 -0
  79. data/lib/action_dispatch/http/parameters.rb +14 -12
  80. data/lib/action_dispatch/http/permissions_policy.rb +25 -36
  81. data/lib/action_dispatch/http/query_parser.rb +55 -0
  82. data/lib/action_dispatch/http/rack_cache.rb +2 -0
  83. data/lib/action_dispatch/http/request.rb +141 -92
  84. data/lib/action_dispatch/http/response.rb +137 -77
  85. data/lib/action_dispatch/http/upload.rb +18 -16
  86. data/lib/action_dispatch/http/url.rb +187 -89
  87. data/lib/action_dispatch/journey/formatter.rb +21 -9
  88. data/lib/action_dispatch/journey/gtg/builder.rb +4 -3
  89. data/lib/action_dispatch/journey/gtg/simulator.rb +34 -11
  90. data/lib/action_dispatch/journey/gtg/transition_table.rb +47 -53
  91. data/lib/action_dispatch/journey/nfa/dot.rb +2 -0
  92. data/lib/action_dispatch/journey/nodes/node.rb +8 -6
  93. data/lib/action_dispatch/journey/parser.rb +99 -195
  94. data/lib/action_dispatch/journey/path/pattern.rb +4 -1
  95. data/lib/action_dispatch/journey/route.rb +54 -38
  96. data/lib/action_dispatch/journey/router/utils.rb +22 -27
  97. data/lib/action_dispatch/journey/router.rb +63 -83
  98. data/lib/action_dispatch/journey/routes.rb +11 -2
  99. data/lib/action_dispatch/journey/scanner.rb +46 -42
  100. data/lib/action_dispatch/journey/visitors.rb +57 -23
  101. data/lib/action_dispatch/journey/visualizer/fsm.js +4 -6
  102. data/lib/action_dispatch/journey.rb +2 -0
  103. data/lib/action_dispatch/log_subscriber.rb +7 -1
  104. data/lib/action_dispatch/middleware/actionable_exceptions.rb +2 -0
  105. data/lib/action_dispatch/middleware/assume_ssl.rb +8 -5
  106. data/lib/action_dispatch/middleware/callbacks.rb +3 -1
  107. data/lib/action_dispatch/middleware/cookies.rb +125 -106
  108. data/lib/action_dispatch/middleware/debug_exceptions.rb +37 -8
  109. data/lib/action_dispatch/middleware/debug_locks.rb +15 -13
  110. data/lib/action_dispatch/middleware/debug_view.rb +13 -5
  111. data/lib/action_dispatch/middleware/exception_wrapper.rb +18 -23
  112. data/lib/action_dispatch/middleware/executor.rb +19 -4
  113. data/lib/action_dispatch/middleware/flash.rb +63 -51
  114. data/lib/action_dispatch/middleware/host_authorization.rb +17 -15
  115. data/lib/action_dispatch/middleware/public_exceptions.rb +14 -12
  116. data/lib/action_dispatch/middleware/reloader.rb +5 -3
  117. data/lib/action_dispatch/middleware/remote_ip.rb +87 -77
  118. data/lib/action_dispatch/middleware/request_id.rb +16 -10
  119. data/lib/action_dispatch/middleware/server_timing.rb +4 -2
  120. data/lib/action_dispatch/middleware/session/abstract_store.rb +2 -0
  121. data/lib/action_dispatch/middleware/session/cache_store.rb +30 -8
  122. data/lib/action_dispatch/middleware/session/cookie_store.rb +27 -26
  123. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +7 -3
  124. data/lib/action_dispatch/middleware/show_exceptions.rb +16 -16
  125. data/lib/action_dispatch/middleware/ssl.rb +53 -40
  126. data/lib/action_dispatch/middleware/stack.rb +11 -10
  127. data/lib/action_dispatch/middleware/static.rb +33 -31
  128. data/lib/action_dispatch/middleware/templates/rescues/_copy_button.html.erb +1 -0
  129. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +3 -5
  130. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +9 -5
  131. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +1 -0
  132. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +1 -0
  133. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +4 -0
  134. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +3 -0
  135. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +50 -0
  136. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +1 -0
  137. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +1 -0
  138. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -0
  139. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -0
  140. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -0
  141. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +1 -1
  142. data/lib/action_dispatch/railtie.rb +23 -3
  143. data/lib/action_dispatch/request/session.rb +24 -21
  144. data/lib/action_dispatch/request/utils.rb +11 -3
  145. data/lib/action_dispatch/routing/endpoint.rb +2 -0
  146. data/lib/action_dispatch/routing/inspector.rb +85 -60
  147. data/lib/action_dispatch/routing/mapper.rb +1031 -851
  148. data/lib/action_dispatch/routing/polymorphic_routes.rb +69 -62
  149. data/lib/action_dispatch/routing/redirection.rb +47 -39
  150. data/lib/action_dispatch/routing/route_set.rb +79 -56
  151. data/lib/action_dispatch/routing/routes_proxy.rb +7 -4
  152. data/lib/action_dispatch/routing/url_for.rb +130 -125
  153. data/lib/action_dispatch/routing.rb +150 -148
  154. data/lib/action_dispatch/structured_event_subscriber.rb +20 -0
  155. data/lib/action_dispatch/system_test_case.rb +91 -81
  156. data/lib/action_dispatch/system_testing/browser.rb +16 -23
  157. data/lib/action_dispatch/system_testing/driver.rb +2 -0
  158. data/lib/action_dispatch/system_testing/server.rb +2 -0
  159. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +34 -23
  160. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +2 -0
  161. data/lib/action_dispatch/testing/assertion_response.rb +9 -7
  162. data/lib/action_dispatch/testing/assertions/response.rb +52 -25
  163. data/lib/action_dispatch/testing/assertions/routing.rb +168 -87
  164. data/lib/action_dispatch/testing/assertions.rb +2 -0
  165. data/lib/action_dispatch/testing/integration.rb +233 -223
  166. data/lib/action_dispatch/testing/request_encoder.rb +11 -9
  167. data/lib/action_dispatch/testing/test_helpers/page_dump_helper.rb +35 -0
  168. data/lib/action_dispatch/testing/test_process.rb +11 -8
  169. data/lib/action_dispatch/testing/test_request.rb +3 -1
  170. data/lib/action_dispatch/testing/test_response.rb +27 -26
  171. data/lib/action_dispatch.rb +36 -32
  172. data/lib/action_pack/gem_version.rb +6 -4
  173. data/lib/action_pack/version.rb +3 -1
  174. data/lib/action_pack.rb +17 -16
  175. metadata +36 -32
  176. data/lib/action_dispatch/journey/parser.y +0 -50
  177. data/lib/action_dispatch/journey/parser_extras.rb +0 -31
@@ -1,33 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  module ActionController
4
- # = Action Controller Implicit Render
6
+ # # Action Controller Implicit Render
5
7
  #
6
- # Handles implicit rendering for a controller action that does not
7
- # explicitly respond with +render+, +respond_to+, +redirect+, or +head+.
8
+ # Handles implicit rendering for a controller action that does not explicitly
9
+ # respond with `render`, `respond_to`, `redirect`, or `head`.
8
10
  #
9
- # For API controllers, the implicit response is always <tt>204 No Content</tt>.
11
+ # For API controllers, the implicit response is always `204 No Content`.
10
12
  #
11
- # For all other controllers, we use these heuristics to decide whether to
12
- # render a template, raise an error for a missing template, or respond with
13
- # <tt>204 No Content</tt>:
13
+ # For all other controllers, we use these heuristics to decide whether to render
14
+ # a template, raise an error for a missing template, or respond with `204 No
15
+ # Content`:
14
16
  #
15
- # First, if we DO find a template, it's rendered. Template lookup accounts
16
- # for the action name, locales, format, variant, template handlers, and more
17
- # (see +render+ for details).
17
+ # First, if we DO find a template, it's rendered. Template lookup accounts for
18
+ # the action name, locales, format, variant, template handlers, and more (see
19
+ # `render` for details).
18
20
  #
19
21
  # Second, if we DON'T find a template but the controller action does have
20
- # templates for other formats, variants, etc., then we trust that you meant
21
- # to provide a template for this response, too, and we raise
22
+ # templates for other formats, variants, etc., then we trust that you meant to
23
+ # provide a template for this response, too, and we raise
22
24
  # ActionController::UnknownFormat with an explanation.
23
25
  #
24
26
  # Third, if we DON'T find a template AND the request is a page load in a web
25
- # browser (technically, a non-XHR GET request for an HTML response) where
26
- # you reasonably expect to have rendered a template, then we raise
27
+ # browser (technically, a non-XHR GET request for an HTML response) where you
28
+ # reasonably expect to have rendered a template, then we raise
27
29
  # ActionController::MissingExactTemplate with an explanation.
28
30
  #
29
31
  # Finally, if we DON'T find a template AND the request isn't a browser page
30
- # load, then we implicitly respond with <tt>204 No Content</tt>.
32
+ # load, then we implicitly respond with `204 No Content`.
31
33
  module ImplicitRender
32
34
  # :stopdoc:
33
35
  include BasicImplicitRender
@@ -1,14 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "benchmark"
3
+ # :markup: markdown
4
+
4
5
  require "abstract_controller/logger"
5
6
 
6
7
  module ActionController
7
- # = Action Controller \Instrumentation
8
+ # # Action Controller Instrumentation
8
9
  #
9
- # Adds instrumentation to several ends in ActionController::Base. It also provides
10
- # some hooks related with process_action. This allows an ORM like Active Record
11
- # and/or DataMapper to plug in ActionController and show related information.
10
+ # Adds instrumentation to several ends in ActionController::Base. It also
11
+ # provides some hooks related with process_action. This allows an ORM like
12
+ # Active Record and/or DataMapper to plug in ActionController and show related
13
+ # information.
12
14
  #
13
15
  # Check ActiveRecord::Railties::ControllerRuntime for an example.
14
16
  module Instrumentation
@@ -26,7 +28,7 @@ module ActionController
26
28
  def render(*)
27
29
  render_output = nil
28
30
  self.view_runtime = cleanup_view_runtime do
29
- Benchmark.ms { render_output = super }
31
+ ActiveSupport::Benchmark.realtime(:float_millisecond) { render_output = super }
30
32
  end
31
33
  render_output
32
34
  end
@@ -91,23 +93,23 @@ module ActionController
91
93
  # A hook which allows you to clean up any time, wrongly taken into account in
92
94
  # views, like database querying time.
93
95
  #
94
- # def cleanup_view_runtime
95
- # super - time_taken_in_something_expensive
96
- # end
96
+ # def cleanup_view_runtime
97
+ # super - time_taken_in_something_expensive
98
+ # end
97
99
  def cleanup_view_runtime # :doc:
98
100
  yield
99
101
  end
100
102
 
101
- # Every time after an action is processed, this method is invoked
102
- # with the payload, so you can add more information.
103
+ # Every time after an action is processed, this method is invoked with the
104
+ # payload, so you can add more information.
103
105
  def append_info_to_payload(payload) # :doc:
104
106
  payload[:view_runtime] = view_runtime
105
107
  end
106
108
 
107
109
  module ClassMethods
108
- # A hook which allows other frameworks to log what happened during
109
- # controller process action. This method should return an array
110
- # with the messages to be added.
110
+ # A hook which allows other frameworks to log what happened during controller
111
+ # process action. This method should return an array with the messages to be
112
+ # added.
111
113
  def log_process_action(payload) # :nodoc:
112
114
  messages, view_runtime = [], payload[:view_runtime]
113
115
  messages << ("Views: %.1fms" % view_runtime.to_f) if view_runtime
@@ -1,62 +1,105 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  require "action_dispatch/http/response"
4
6
  require "delegate"
5
7
  require "active_support/json"
6
8
 
7
9
  module ActionController
8
- # = Action Controller \Live
10
+ # # Action Controller Live
9
11
  #
10
- # Mix this module into your controller, and all actions in that controller
11
- # will be able to stream data to the client as it's written.
12
+ # Mix this module into your controller, and all actions in that controller will
13
+ # be able to stream data to the client as it's written.
12
14
  #
13
- # class MyController < ActionController::Base
14
- # include ActionController::Live
15
+ # class MyController < ActionController::Base
16
+ # include ActionController::Live
15
17
  #
16
- # def stream
17
- # response.headers['Content-Type'] = 'text/event-stream'
18
- # 100.times {
19
- # response.stream.write "hello world\n"
20
- # sleep 1
21
- # }
22
- # ensure
23
- # response.stream.close
18
+ # def stream
19
+ # response.headers['Content-Type'] = 'text/event-stream'
20
+ # 100.times {
21
+ # response.stream.write "hello world\n"
22
+ # sleep 1
23
+ # }
24
+ # ensure
25
+ # response.stream.close
26
+ # end
24
27
  # end
25
- # end
26
28
  #
27
- # There are a few caveats with this module. You *cannot* write headers after the
28
- # response has been committed (Response#committed? will return truthy).
29
- # Calling +write+ or +close+ on the response stream will cause the response
30
- # object to be committed. Make sure all headers are set before calling write
31
- # or close on your stream.
29
+ # There are a few caveats with this module. You **cannot** write headers after
30
+ # the response has been committed (Response#committed? will return truthy).
31
+ # Calling `write` or `close` on the response stream will cause the response
32
+ # object to be committed. Make sure all headers are set before calling write or
33
+ # close on your stream.
32
34
  #
33
- # You *must* call close on your stream when you're finished, otherwise the
35
+ # You **must** call close on your stream when you're finished, otherwise the
34
36
  # socket may be left open forever.
35
37
  #
36
38
  # The final caveat is that your actions are executed in a separate thread than
37
- # the main thread. Make sure your actions are thread safe, and this shouldn't
38
- # be a problem (don't share state across threads, etc).
39
+ # the main thread. Make sure your actions are thread safe, and this shouldn't be
40
+ # a problem (don't share state across threads, etc).
39
41
  #
40
- # Note that \Rails includes +Rack::ETag+ by default, which will buffer your
42
+ # Note that Rails includes `Rack::ETag` by default, which will buffer your
41
43
  # response. As a result, streaming responses may not work properly with Rack
42
- # 2.2.x, and you may need to implement workarounds in your application.
43
- # You can either set the +ETag+ or +Last-Modified+ response headers or remove
44
- # +Rack::ETag+ from the middleware stack to address this issue.
44
+ # 2.2.x, and you may need to implement workarounds in your application. You can
45
+ # either set the `ETag` or `Last-Modified` response headers or remove
46
+ # `Rack::ETag` from the middleware stack to address this issue.
45
47
  #
46
- # Here's an example of how you can set the +Last-Modified+ header if your Rack
48
+ # Here's an example of how you can set the `Last-Modified` header if your Rack
47
49
  # version is 2.2.x:
48
50
  #
49
- # def stream
50
- # response.headers["Content-Type"] = "text/event-stream"
51
- # response.headers["Last-Modified"] = Time.now.httpdate # Add this line if your Rack version is 2.2.x
52
- # ...
51
+ # def stream
52
+ # response.headers["Content-Type"] = "text/event-stream"
53
+ # response.headers["Last-Modified"] = Time.now.httpdate # Add this line if your Rack version is 2.2.x
54
+ # ...
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
53
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.
54
91
  module Live
55
92
  extend ActiveSupport::Concern
56
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
+
57
100
  module ClassMethods
58
101
  def make_response!(request)
59
- if request.get_header("HTTP_VERSION") == "HTTP/1.0"
102
+ if (request.get_header("SERVER_PROTOCOL") || request.get_header("HTTP_VERSION")) == "HTTP/1.0"
60
103
  super
61
104
  else
62
105
  Live::Response.new.tap do |res|
@@ -66,44 +109,47 @@ module ActionController
66
109
  end
67
110
  end
68
111
 
69
- # = Action Controller \Live Server Sent Events
112
+ # # Action Controller Live Server Sent Events
70
113
  #
71
- # This class provides the ability to write an SSE (Server Sent Event)
72
- # to an IO stream. The class is initialized with a stream and can be used
73
- # to either write a JSON string or an object which can be converted to JSON.
114
+ # This class provides the ability to write an SSE (Server Sent Event) to an IO
115
+ # stream. The class is initialized with a stream and can be used to either write
116
+ # a JSON string or an object which can be converted to JSON.
74
117
  #
75
118
  # Writing an object will convert it into standard SSE format with whatever
76
119
  # options you have configured. You may choose to set the following options:
77
120
  #
78
- # 1) Event. If specified, an event with this name will be dispatched on
79
- # the browser.
80
- # 2) Retry. The reconnection time in milliseconds used when attempting
81
- # to send the event.
82
- # 3) Id. If the connection dies while sending an SSE to the browser, then
83
- # the server will receive a +Last-Event-ID+ header with value equal to +id+.
121
+ # `:event`
122
+ # : If specified, an event with this name will be dispatched on the browser.
84
123
  #
85
- # After setting an option in the constructor of the SSE object, all future
86
- # SSEs sent across the stream will use those options unless overridden.
124
+ # `:retry`
125
+ # : The reconnection time in milliseconds used when attempting to send the event.
126
+ #
127
+ # `:id`
128
+ # : If the connection dies while sending an SSE to the browser, then the
129
+ # server will receive a `Last-Event-ID` header with value equal to `id`.
130
+ #
131
+ # After setting an option in the constructor of the SSE object, all future SSEs
132
+ # sent across the stream will use those options unless overridden.
87
133
  #
88
134
  # Example Usage:
89
135
  #
90
- # class MyController < ActionController::Base
91
- # include ActionController::Live
136
+ # class MyController < ActionController::Base
137
+ # include ActionController::Live
92
138
  #
93
- # def index
94
- # response.headers['Content-Type'] = 'text/event-stream'
95
- # sse = SSE.new(response.stream, retry: 300, event: "event-name")
96
- # sse.write({ name: 'John'})
97
- # sse.write({ name: 'John'}, id: 10)
98
- # sse.write({ name: 'John'}, id: 10, event: "other-event")
99
- # sse.write({ name: 'John'}, id: 10, event: "other-event", retry: 500)
100
- # ensure
101
- # sse.close
139
+ # def index
140
+ # response.headers['Content-Type'] = 'text/event-stream'
141
+ # sse = SSE.new(response.stream, retry: 300, event: "event-name")
142
+ # sse.write({ name: 'John'})
143
+ # sse.write({ name: 'John'}, id: 10)
144
+ # sse.write({ name: 'John'}, id: 10, event: "other-event")
145
+ # sse.write({ name: 'John'}, id: 10, event: "other-event", retry: 500)
146
+ # ensure
147
+ # sse.close
148
+ # end
102
149
  # end
103
- # end
104
150
  #
105
- # Note: SSEs are not currently supported by IE. However, they are supported
106
- # by Chrome, Firefox, Opera, and Safari.
151
+ # Note: SSEs are not currently supported by IE. However, they are supported by
152
+ # Chrome, Firefox, Opera, and Safari.
107
153
  class SSE
108
154
  PERMITTED_OPTIONS = %w( retry event id )
109
155
 
@@ -128,15 +174,16 @@ module ActionController
128
174
  private
129
175
  def perform_write(json, options)
130
176
  current_options = @options.merge(options).stringify_keys
131
-
177
+ event = +""
132
178
  PERMITTED_OPTIONS.each do |option_name|
133
179
  if (option_value = current_options[option_name])
134
- @stream.write "#{option_name}: #{option_value}\n"
180
+ event << "#{option_name}: #{option_value}\n"
135
181
  end
136
182
  end
137
183
 
138
184
  message = json.gsub("\n", "\ndata: ")
139
- @stream.write "data: #{message}\n\n"
185
+ event << "data: #{message}\n\n"
186
+ @stream.write event
140
187
  end
141
188
  end
142
189
 
@@ -153,10 +200,9 @@ module ActionController
153
200
 
154
201
  # Ignore that the client has disconnected.
155
202
  #
156
- # If this value is `true`, calling `write` after the client
157
- # disconnects will result in the written content being silently
158
- # discarded. If this value is `false` (the default), a
159
- # ClientDisconnected exception will be raised.
203
+ # If this value is `true`, calling `write` after the client disconnects will
204
+ # result in the written content being silently discarded. If this value is
205
+ # `false` (the default), a ClientDisconnected exception will be raised.
160
206
  attr_accessor :ignore_disconnect
161
207
 
162
208
  def initialize(response)
@@ -167,11 +213,6 @@ module ActionController
167
213
  @ignore_disconnect = false
168
214
  end
169
215
 
170
- # ActionDispatch::Response delegates #to_ary to the internal ActionDispatch::Response::Buffer,
171
- # defining #to_ary is an indicator that the response body can be buffered and/or cached by
172
- # Rack middlewares, this is not the case for Live responses so we undefine it for this Buffer subclass.
173
- undef_method :to_ary
174
-
175
216
  def write(string)
176
217
  unless @response.committed?
177
218
  @response.headers["Cache-Control"] ||= "no-cache"
@@ -184,21 +225,20 @@ module ActionController
184
225
  @buf.clear
185
226
 
186
227
  unless @ignore_disconnect
187
- # Raise ClientDisconnected, which is a RuntimeError (not an
188
- # IOError), because that's more appropriate for something beyond
189
- # the developer's control.
228
+ # Raise ClientDisconnected, which is a RuntimeError (not an IOError), because
229
+ # that's more appropriate for something beyond the developer's control.
190
230
  raise ClientDisconnected, "client disconnected"
191
231
  end
192
232
  end
193
233
  end
194
234
 
195
- # Same as +write+ but automatically include a newline at the end of the string.
235
+ # Same as `write` but automatically include a newline at the end of the string.
196
236
  def writeln(string)
197
237
  write string.end_with?("\n") ? string : "#{string}\n"
198
238
  end
199
239
 
200
- # Write a 'close' event to the buffer; the producer/writing thread
201
- # uses this to notify us that it's finished supplying content.
240
+ # Write a 'close' event to the buffer; the producer/writing thread uses this to
241
+ # notify us that it's finished supplying content.
202
242
  #
203
243
  # See also #abort.
204
244
  def close
@@ -209,9 +249,8 @@ module ActionController
209
249
  end
210
250
  end
211
251
 
212
- # Inform the producer/writing thread that the client has
213
- # disconnected; the reading thread is no longer interested in
214
- # anything that's being written.
252
+ # Inform the producer/writing thread that the client has disconnected; the
253
+ # reading thread is no longer interested in anything that's being written.
215
254
  #
216
255
  # See also #close.
217
256
  def abort
@@ -223,8 +262,8 @@ module ActionController
223
262
 
224
263
  # Is the client still connected and waiting for content?
225
264
  #
226
- # The result of calling `write` when this is `false` is determined
227
- # by `ignore_disconnect`.
265
+ # The result of calling `write` when this is `false` is determined by
266
+ # `ignore_disconnect`.
228
267
  def connected?
229
268
  !@aborted
230
269
  end
@@ -239,12 +278,7 @@ module ActionController
239
278
 
240
279
  private
241
280
  def each_chunk(&block)
242
- loop do
243
- str = nil
244
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
245
- str = @buf.pop
246
- end
247
- break unless str
281
+ while str = @buf.pop
248
282
  yield str
249
283
  end
250
284
  end
@@ -275,19 +309,18 @@ module ActionController
275
309
  locals = t1.keys.map { |key| [key, t1[key]] }
276
310
 
277
311
  error = nil
278
- # This processes the action in a child thread. It lets us return the
279
- # response code and headers back up the Rack stack, and still process
280
- # the body in parallel with sending data to the client.
281
- new_controller_thread {
312
+ # This processes the action in a child thread. It lets us return the response
313
+ # code and headers back up the Rack stack, and still process the body in
314
+ # parallel with sending data to the client.
315
+ new_controller_thread do
282
316
  ActiveSupport::Dependencies.interlock.running do
283
317
  t2 = Thread.current
284
318
 
285
- # Since we're processing the view in a different thread, copy the
286
- # thread locals from the main thread to the child thread. :'(
319
+ # Since we're processing the view in a different thread, copy the thread locals
320
+ # from the main thread to the child thread. :'(
287
321
  locals.each { |k, v| t2[k] = v }
288
- ActiveSupport::IsolatedExecutionState.share_with(t1)
289
322
 
290
- begin
323
+ ActiveSupport::IsolatedExecutionState.share_with(t1, except: self.class.live_streaming_excluded_keys) do
291
324
  super(name)
292
325
  rescue => e
293
326
  if @_response.committed?
@@ -304,15 +337,15 @@ module ActionController
304
337
  error = e
305
338
  end
306
339
  ensure
340
+ clean_up_thread_locals(locals, t2)
341
+
307
342
  @_response.commit!
308
343
  end
309
344
  end
310
- }
311
-
312
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
313
- @_response.await_commit
314
345
  end
315
346
 
347
+ @_response.await_commit
348
+
316
349
  raise error if error
317
350
  end
318
351
 
@@ -321,52 +354,68 @@ module ActionController
321
354
  response.close if response
322
355
  end
323
356
 
324
- # Sends a stream to the browser, which is helpful when you're generating exports or other running data where you
325
- # don't want the entire file buffered in memory first. Similar to send_data, but where the data is generated live.
357
+ # Sends a stream to the browser, which is helpful when you're generating exports
358
+ # or other running data where you don't want the entire file buffered in memory
359
+ # first. Similar to send_data, but where the data is generated live.
360
+ #
361
+ # #### Options:
362
+ #
363
+ # * `:filename` - suggests a filename for the browser to use.
364
+ # * `:type` - specifies an HTTP content type. You can specify either a string
365
+ # or a symbol for a registered type with `Mime::Type.register`, for example
366
+ # :json. If omitted, type will be inferred from the file extension specified
367
+ # in `:filename`. If no content type is registered for the extension, the
368
+ # default type 'application/octet-stream' will be used.
369
+ # * `:disposition` - specifies whether the file will be shown inline or
370
+ # downloaded. Valid values are 'inline' and 'attachment' (default).
326
371
  #
327
- # Options:
328
- # * <tt>:filename</tt> - suggests a filename for the browser to use.
329
- # * <tt>:type</tt> - specifies an HTTP content type.
330
- # You can specify either a string or a symbol for a registered type with <tt>Mime::Type.register</tt>, for example :json.
331
- # If omitted, type will be inferred from the file extension specified in <tt>:filename</tt>.
332
- # If no content type is registered for the extension, the default type 'application/octet-stream' will be used.
333
- # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
334
- # Valid values are 'inline' and 'attachment' (default).
335
372
  #
336
373
  # Example of generating a csv export:
337
374
  #
338
- # send_stream(filename: "subscribers.csv") do |stream|
339
- # stream.write "email_address,updated_at\n"
375
+ # send_stream(filename: "subscribers.csv") do |stream|
376
+ # stream.write "email_address,updated_at\n"
340
377
  #
341
- # @subscribers.find_each do |subscriber|
342
- # stream.write "#{subscriber.email_address},#{subscriber.updated_at}\n"
343
- # end
344
- # end
378
+ # @subscribers.find_each do |subscriber|
379
+ # stream.write "#{subscriber.email_address},#{subscriber.updated_at}\n"
380
+ # end
381
+ # end
345
382
  def send_stream(filename:, disposition: "attachment", type: nil)
346
- response.headers["Content-Type"] =
347
- (type.is_a?(Symbol) ? Mime[type].to_s : type) ||
348
- Mime::Type.lookup_by_extension(File.extname(filename).downcase.delete("."))&.to_s ||
349
- "application/octet-stream"
383
+ payload = { filename: filename, disposition: disposition, type: type }
384
+ ActiveSupport::Notifications.instrument("send_stream.action_controller", payload) do
385
+ response.headers["Content-Type"] =
386
+ (type.is_a?(Symbol) ? Mime[type].to_s : type) ||
387
+ Mime::Type.lookup_by_extension(File.extname(filename).downcase.delete("."))&.to_s ||
388
+ "application/octet-stream"
350
389
 
351
- response.headers["Content-Disposition"] =
352
- ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: filename)
390
+ response.headers["Content-Disposition"] =
391
+ ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: filename)
353
392
 
354
- yield response.stream
393
+ yield response.stream
394
+ end
355
395
  ensure
356
396
  response.stream.close
357
397
  end
358
398
 
359
399
  private
360
- # Spawn a new thread to serve up the controller in. This is to get
361
- # around the fact that Rack isn't based around IOs and we need to use
362
- # a thread to stream data from the response bodies. Nobody should call
363
- # this method except in Rails internals. Seriously!
400
+ # Spawn a new thread to serve up the controller in. This is to get around the
401
+ # fact that Rack isn't based around IOs and we need to use a thread to stream
402
+ # data from the response bodies. Nobody should call this method except in Rails
403
+ # internals. Seriously!
364
404
  def new_controller_thread # :nodoc:
365
- Thread.new {
405
+ ActionController::Live.live_thread_pool_executor.post do
366
406
  t2 = Thread.current
367
407
  t2.abort_on_exception = true
368
408
  yield
369
- }
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")
370
419
  end
371
420
 
372
421
  def log_error(exception)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  module ActionController
4
6
  module Logging
5
7
  extend ActiveSupport::Concern
@@ -7,10 +9,10 @@ module ActionController
7
9
  module ClassMethods
8
10
  # Set a different log level per request.
9
11
  #
10
- # # Use the debug log level if a particular cookie is set.
11
- # class ApplicationController < ActionController::Base
12
- # log_at :debug, if: -> { cookies[:debug] }
13
- # end
12
+ # # Use the debug log level if a particular cookie is set.
13
+ # class ApplicationController < ActionController::Base
14
+ # log_at :debug, if: -> { cookies[:debug] }
15
+ # end
14
16
  #
15
17
  def log_at(level, **options)
16
18
  around_action ->(_, action) { logger.log_at(level, &action) }, **options