omg-actionpack 8.0.0.alpha1

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