omg-actionpack 8.0.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
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