diff options
Diffstat (limited to 'actionpack/lib/action_controller/metal/streaming.rb')
-rw-r--r-- | actionpack/lib/action_controller/metal/streaming.rb | 372 |
1 files changed, 245 insertions, 127 deletions
diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb index 312dc8eb3e..3892a12407 100644 --- a/actionpack/lib/action_controller/metal/streaming.rb +++ b/actionpack/lib/action_controller/metal/streaming.rb @@ -1,145 +1,263 @@ require 'active_support/core_ext/file/path' +require 'rack/chunked' module ActionController #:nodoc: - # Methods for sending arbitrary data and for streaming files to the browser, - # instead of rendering. + # Allows views to be streamed back to the client as they are rendered. + # + # The default way Rails renders views is 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 + # streaming 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, allowing JavaScripts and stylesheets to be loaded + # earlier than usual. + # + # This approach was introduced in Rails 3.1 and is still improving. Several + # Rack middlewares may not work and you need to be careful when streaming. + # Those points are going to be addressed soon. + # + # In order to use streaming, you will need to use a Ruby version that + # supports fibers (fibers are supported since version 1.9.2 of the main + # Ruby implementation). + # + # == Examples + # + # Streaming can be added to a controller easily, all you need to do is + # call +stream+ in the controller class: + # + # class PostsController + # stream + # end + # + # The +stream+ method accepts the same options as +before_filter+ and friends: + # + # class PostsController + # stream :only => :index + # end + # + # You can also selectively turn on streaming for specific actions: + # + # class PostsController + # def index + # @posts = Post.scoped + # 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.scoped + # @pages = Page.scoped + # @articles = Article.scoped + # render :stream => true + # end + # + # == 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: + # + # <html> + # <head><title><%= yield :title %></title></head> + # <body><%= yield %></body> + # </html> + # + # You would use +content_for+ in your template to specify the title: + # + # <%= content_for :title, "Main" %> + # Hello + # + # And the final result would be: + # + # <html> + # <head><title>Main</title></head> + # <body>Hello</body> + # </html> + # + # 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: + # + # <html> + # <head><title>Main page</title></head> + # <body>Hello</body> + # </html> + # + # This means that, if you have <code>yield :title</code> 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 kills the purpose of streaming. For this reason Rails 3.1 + # introduces a new 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" %> + # + # Giving: + # + # <html> + # <head><title>Main</title></head> + # <body>Hello</body> + # </html> + # + # That said, when streaming, you need to properly check your templates + # and choose when to use +provide+ and +content_for+. + # + # == 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. + # + # If you try to modify cookies, session or flash, an +ActionDispatch::ClosedError+ + # will be raised, showing those objects are closed for modification. + # + # == 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: + # + # "><script type="text/javascript">window.location = "/500.html"</script></html> + # + # 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 <tt>:tcp_nodelay</tt>. + # Please check its documentation for more information: http://unicorn.bogomips.org/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 + # + # To be described. + # module Streaming extend ActiveSupport::Concern - include ActionController::Rendering + include AbstractController::Rendering + attr_internal :stream - DEFAULT_SEND_FILE_OPTIONS = { - :type => 'application/octet-stream'.freeze, - :disposition => 'attachment'.freeze, - }.freeze - - protected - # Sends the file. This uses a server-appropriate method (such as X-Sendfile) - # via the Rack::Sendfile middleware. The header to use is set via - # config.action_dispatch.x_sendfile_header, and defaults to "X-Sendfile". - # Your server can also configure this for you by setting the X-Sendfile-Type header. - # - # Be careful to sanitize the path parameter if it is coming from a web - # page. <tt>send_file(params[:path])</tt> allows a malicious user to - # download any file on your server. - # - # Options: - # * <tt>:filename</tt> - suggests a filename for the browser to use. - # Defaults to <tt>File.basename(path)</tt>. - # * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'. You can specify - # either a string or a symbol for a registered type register with <tt>Mime::Type.register</tt>, for example :json - # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded. - # Valid values are 'inline' and 'attachment' (default). - # * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'. - # * <tt>:url_based_filename</tt> - set to +true+ if you want the browser guess the filename from - # the URL, which is necessary for i18n filenames on certain browsers - # (setting <tt>:filename</tt> overrides this option). - # - # The default Content-Type and Content-Disposition headers are - # set to download arbitrary binary files in as many browsers as - # possible. IE versions 4, 5, 5.5, and 6 are all known to have - # a variety of quirks (especially when downloading over SSL). - # - # Simple download: - # - # send_file '/path/to.zip' - # - # Show a JPEG in the browser: - # - # send_file '/path/to.jpeg', :type => 'image/jpeg', :disposition => 'inline' - # - # Show a 404 page in the browser: - # - # send_file '/path/to/404.html', :type => 'text/html; charset=utf-8', :status => 404 - # - # Read about the other Content-* HTTP headers if you'd like to - # provide the user with more information (such as Content-Description) in - # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11. - # - # Also be aware that the document may be cached by proxies and browsers. - # The Pragma and Cache-Control headers declare how the file may be cached - # by intermediaries. They default to require clients to validate with - # the server before releasing cached responses. See - # http://www.mnot.net/cache_docs/ for an overview of web caching and - # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9 - # for the Cache-Control header spec. - def send_file(path, options = {}) #:doc: - raise MissingFile, "Cannot read file #{path}" unless File.file?(path) and File.readable?(path) - - options[:filename] ||= File.basename(path) unless options[:url_based_filename] - send_file_headers! options - - self.status = options[:status] || 200 - self.content_type = options[:content_type] if options.key?(:content_type) - self.response_body = File.open(path, "rb") - end - - # Sends the given binary data to the browser. This method is similar to - # <tt>render :text => data</tt>, but also allows you to specify whether - # the browser should display the response as a file attachment (i.e. in a - # download dialog) or as inline data. You may also set the content type, - # the apparent file name, and other things. - # - # Options: - # * <tt>:filename</tt> - suggests a filename for the browser to use. - # * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'. You can specify - # either a string or a symbol for a registered type register with <tt>Mime::Type.register</tt>, for example :json - # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded. - # Valid values are 'inline' and 'attachment' (default). - # * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'. - # - # Generic data download: - # - # send_data buffer - # - # Download a dynamically-generated tarball: - # - # send_data generate_tgz('dir'), :filename => 'dir.tgz' - # - # Display an image Active Record in the browser: - # - # send_data image.data, :type => image.content_type, :disposition => 'inline' - # - # See +send_file+ for more information on HTTP Content-* headers and caching. - def send_data(data, options = {}) #:doc: - send_file_headers! options.dup - render options.slice(:status, :content_type).merge(:text => data) + module ClassMethods + # Render streaming templates. It accepts :only, :except, :if and :unless as options + # to specify when to stream, as in ActionController filters. + def stream(options={}) + if defined?(Fiber) + before_filter :_stream_filter, options + else + raise "You cannot use streaming if Fiber is not available." + end end + end - private - def send_file_headers!(options) - options.update(DEFAULT_SEND_FILE_OPTIONS.merge(options)) - [:type, :disposition].each do |arg| - raise ArgumentError, ":#{arg} option required" if options[arg].nil? - end + protected - disposition = options[:disposition] - disposition += %(; filename="#{options[:filename]}") if options[:filename] + # Mark following render calls as streaming. + def _stream_filter #:nodoc: + self.stream = true + end - content_type = options[:type] + # Consider the stream option when normalazing options. + def _normalize_options(options) #:nodoc: + super + options[:stream] = self.stream unless options.key?(:stream) + end - if content_type.is_a?(Symbol) - extension = Mime[content_type] - raise ArgumentError, "Unknown MIME type #{options[:type]}" unless extension - self.content_type = extension + # Set proper cache control and transfer encoding when streaming + def _process_options(options) #:nodoc: + super + if options[:stream] + if env["HTTP_VERSION"] == "HTTP/1.0" + options.delete(:stream) else - self.content_type = content_type + headers["Cache-Control"] ||= "no-cache" + headers["Transfer-Encoding"] = "chunked" + headers.delete("Content-Length") end + end + end - headers.merge!( - 'Content-Disposition' => disposition, - 'Content-Transfer-Encoding' => 'binary' - ) - - response.sending_file = true - - # Fix a problem with IE 6.0 on opening downloaded files: - # If Cache-Control: no-cache is set (which Rails does by default), - # IE removes the file it just downloaded from its cache immediately - # after it displays the "open/save" dialog, which means that if you - # hit "open" the file isn't there anymore when the application that - # is called for handling the download is run, so let's workaround that - response.cache_control[:public] ||= false + # Call render_to_body if we are streaming instead of usual +render+. + def _render_template(options) #:nodoc: + if options.delete(:stream) + Rack::Chunked::Body.new view_renderer.render_body(view_context, options) + else + super end + end end end +
\ No newline at end of file |