# frozen_string_literal: true module ActionController # :nodoc: # = Action Controller \Streaming # # Allows views to be streamed back to the client as they are rendered. # # By default, \Rails renders views by first rendering the template # and then the layout. The response is sent to the client after the whole # template is rendered, all queries are made, and the layout is processed. # # \Streaming inverts the rendering flow by rendering the layout first and # subsequently each part of the layout as they are processed. This allows the # header of the HTML (which is usually in the layout) to be streamed back # to client very quickly, enabling JavaScripts and stylesheets to be loaded # earlier than usual. # # Several Rack middlewares may not work and you need to be careful when streaming. # This is covered in more detail below, see the Streaming@Middlewares section. # # \Streaming can be added to a given template easily, all you need to do is # to pass the +:stream+ option to +render+. # # class PostsController # def index # @posts = Post.all # render stream: true # end # end # # == When to use streaming # # \Streaming may be considered to be overkill for lightweight actions like # +new+ or +edit+. The real benefit of streaming is on expensive actions # that, for example, do a lot of queries on the database. # # In such actions, you want to delay queries execution as much as you can. # For example, imagine the following +dashboard+ action: # # def dashboard # @posts = Post.all # @pages = Page.all # @articles = Article.all # end # # Most of the queries here are happening in the controller. In order to benefit # from streaming you would want to rewrite it as: # # def dashboard # # Allow lazy execution of the queries # @posts = Post.all # @pages = Page.all # @articles = Article.all # render stream: true # end # # Notice that +:stream+ only works with templates. \Rendering +:json+ # or +:xml+ with +:stream+ won't work. # # == Communication between layout and template # # When streaming, rendering happens top-down instead of inside-out. # \Rails starts with the layout, and the template is rendered later, # when its +yield+ is reached. # # This means that, if your application currently relies on instance # variables set in the template to be used in the layout, they won't # work once you move to streaming. The proper way to communicate # between layout and template, regardless of whether you use streaming # or not, is by using +content_for+, +provide+, and +yield+. # # Take a simple example where the layout expects the template to tell # which title to use: # # # <%= yield :title %> # <%= yield %> # # # You would use +content_for+ in your template to specify the title: # # <%= content_for :title, "Main" %> # Hello # # And the final result would be: # # # Main # Hello # # # However, if +content_for+ is called several times, the final result # would have all calls concatenated. For instance, if we have the following # template: # # <%= content_for :title, "Main" %> # Hello # <%= content_for :title, " page" %> # # The final result would be: # # # Main page # Hello # # # This means that, if you have yield :title in your layout # and you want to use streaming, you would have to render the whole template # (and eventually trigger all queries) before streaming the title and all # assets, which defeats the purpose of streaming. Alternatively, you can use # a helper called +provide+ that does the same as +content_for+ but tells the # layout to stop searching for other entries and continue rendering. # # For instance, the template above using +provide+ would be: # # <%= provide :title, "Main" %> # Hello # <%= content_for :title, " page" %> # # Resulting in: # # # Main # Hello # # # That said, when streaming, you need to properly check your templates # and choose when to use +provide+ and +content_for+. # # See also ActionView::Helpers::CaptureHelper for more information. # # == Headers, cookies, session, and flash # # When streaming, the HTTP headers are sent to the client right before # it renders the first line. This means that, modifying headers, cookies, # session or flash after the template starts rendering will not propagate # to the client. # # == Middlewares # # Middlewares that need to manipulate the body won't work with streaming. # You should disable those middlewares whenever streaming in development # or production. For instance, +Rack::Bug+ won't work when streaming as it # needs to inject contents in the HTML body. # # Also +Rack::Cache+ won't work with streaming as it does not support # streaming bodies yet. Whenever streaming +Cache-Control+ is automatically # set to "no-cache". # # == Errors # # When it comes to streaming, exceptions get a bit more complicated. This # happens because part of the template was already rendered and streamed to # the client, making it impossible to render a whole exception page. # # Currently, when an exception happens in development or production, \Rails # will automatically stream to the client: # # "> # # The first two characters (">) are required in case the exception # happens while rendering attributes for a given tag. You can check the real # cause for the exception in your logger. # # == Web server support # # Not all web servers support streaming out-of-the-box. You need to check # the instructions for each of them. # # ==== Unicorn # # Unicorn supports streaming but it needs to be configured. For this, you # need to create a config file as follow: # # # unicorn.config.rb # listen 3000, tcp_nopush: false # # And use it on initialization: # # unicorn_rails --config-file unicorn.config.rb # # You may also want to configure other parameters like :tcp_nodelay. # # For more information, please check the # {documentation}[https://bogomips.org/unicorn/Unicorn/Configurator.html#method-i-listen]. # # If you are using Unicorn with NGINX, you may need to tweak NGINX. # \Streaming should work out of the box on Rainbows. # # ==== Passenger # # Phusion Passenger with NGINX, offers two streaming mechanisms out of the box. # # 1. NGINX response buffering mechanism which is dependent on the value of # +passenger_buffer_response+ option (default is "off"). # 2. Passenger buffering system which is always 'on' irrespective of the value # of +passenger_buffer_response+. # # When +passenger_buffer_response+ is turned "on", then streaming would be # done at the NGINX level which waits until the application is done sending # the response back to the client. # # For more information, please check the # {documentation}[https://www.phusionpassenger.com/docs/references/config_reference/nginx/#passenger_buffer_response]. # module Streaming class Body # :nodoc: TERM = "\r\n" TAIL = "0#{TERM}" # Store the response body to be chunked. def initialize(body) @body = body end # For each element yielded by the response body, yield # the element in chunked encoding. def each(&block) term = TERM @body.each do |chunk| size = chunk.bytesize next if size == 0 yield [size.to_s(16), term, chunk.b, term].join end yield TAIL yield term end # Close the response body if the response body supports it. def close @body.close if @body.respond_to?(:close) end end private # Set proper cache control and transfer encoding when streaming def _process_options(options) super if options[:stream] if request.version == "HTTP/1.0" options.delete(:stream) else headers["Cache-Control"] ||= "no-cache" headers["Transfer-Encoding"] = "chunked" headers.delete("Content-Length") end end end # Call render_body if we are streaming instead of usual +render+. def _render_template(options) if options.delete(:stream) Body.new view_renderer.render_body(view_context, options) else super end end end end