Merge pull request #52094 from ioquatix/rack-3-streaming

Utilize Rack 3 streaming.
This commit is contained in:
Aaron Patterson 2024-07-01 17:39:11 -07:00 committed by GitHub
commit 35a4946d1c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 12 additions and 105 deletions

@ -165,95 +165,16 @@ module ActionController # :nodoc:
#
# ## 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).
# Rack 3+ compatible servers all support streaming.
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)
# It shoudn't be necessary to set this.
headers["cache-control"] ||= "no-cache"
view_renderer.render_body(view_context, options)
else
super
end

@ -44,31 +44,27 @@ def explicit_cache
end
class StreamingTest < Rack::TestCase
def get(path, headers: { "SERVER_PROTOCOL" => "HTTP/1.1", "HTTP_VERSION" => "HTTP/1.1" })
super
end
test "rendering with streaming enabled at the class level" do
get "/render_streaming/basic/hello_world"
assert_body "b\r\nHello world\r\nb\r\n, I'm here!\r\n0\r\n\r\n"
assert_body "Hello world, I'm here!"
assert_streaming!
end
test "rendering with streaming given to render" do
get "/render_streaming/basic/explicit"
assert_body "b\r\nHello world\r\nb\r\n, I'm here!\r\n0\r\n\r\n"
assert_body "Hello world, I'm here!"
assert_streaming!
end
test "rendering with streaming do not override explicit cache control given to render" do
get "/render_streaming/basic/explicit_cache"
assert_body "b\r\nHello world\r\nb\r\n, I'm here!\r\n0\r\n\r\n"
assert_body "Hello world, I'm here!"
assert_streaming! "private"
end
test "rendering with streaming no layout" do
get "/render_streaming/basic/no_layout"
assert_body "b\r\nHello world\r\n0\r\n\r\n"
assert_body "Hello world"
assert_streaming!
end
@ -79,13 +75,13 @@ def get(path, headers: { "SERVER_PROTOCOL" => "HTTP/1.1", "HTTP_VERSION" => "HTT
test "rendering with layout exception" do
get "/render_streaming/basic/layout_exception"
assert_body "d\r\n<body class=\"\r\n37\r\n\"><script>window.location = \"/500.html\"</script></html>\r\n0\r\n\r\n"
assert_body "<body class=\"\"><script>window.location = \"/500.html\"</script></html>"
assert_streaming!
end
test "rendering with template exception" do
get "/render_streaming/basic/template_exception"
assert_body "37\r\n\"><script>window.location = \"/500.html\"</script></html>\r\n0\r\n\r\n"
assert_body "\"><script>window.location = \"/500.html\"</script></html>"
assert_streaming!
end
@ -102,19 +98,9 @@ def get(path, headers: { "SERVER_PROTOCOL" => "HTTP/1.1", "HTTP_VERSION" => "HTT
end
end
test "do not stream on HTTP/1.0" do
get "/render_streaming/basic/hello_world", headers: { "HTTP_VERSION" => "HTTP/1.0" }
assert_body "Hello world, I'm here!"
assert_status 200
assert_equal "22", headers["Content-Length"]
assert_nil headers["Transfer-Encoding"]
end
def assert_streaming!(cache = "no-cache")
assert_status 200
assert_nil headers["Content-Length"]
assert_equal "chunked", headers["Transfer-Encoding"]
assert_equal cache, headers["Cache-Control"]
assert_equal cache, headers["cache-control"]
end
end
end