Skip to content

Commit

Permalink
Explain zip_kit_stream better
Browse files Browse the repository at this point in the history
and enable explicit chunking with ActionController::Live
  • Loading branch information
julik committed Mar 26, 2024
1 parent 7dc951c commit 8892a7b
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 19 deletions.
54 changes: 37 additions & 17 deletions lib/zip_kit/rails_streaming.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,28 @@ module ZipKit::RailsStreaming
# Opens a {ZipKit::Streamer} and yields it to the caller. The output of the streamer
# gets automatically forwarded to the Rails response stream. When the output completes,
# the Rails response stream is going to be closed automatically.
#
# Note that there is an important difference in how this method works, depending whether
# you use it in a controller which includes `ActionController::Live` vs. one that does not.
# With a standard `ActionController` this method will assign a response body, but streaming
# will begin when your action method returns. With `ActionController::Live` the streaming
# will begin immediately, before the method returns. In all other aspects the method should
# stream correctly in both types of controllers.
#
# If you encounter buffering (streaming does not start for a very long time) you probably
# have a piece of Rack middleware in your stack which buffers. Known offenders are `Rack::ContentLength`,
# `Rack::MiniProfiler` and `Rack::ETag`. ZipKit will try to work around these but it is not
# always possible. If you encounter buffering, examine your middleware stack and try to suss
# out whether any middleware might be buffering. You can also try setting `use_chunked_transfer_encoding`
# to `true` - this is not recommended but sometimes necessary, for example to bypass `Rack::ContentLength`.
#
# @param filename[String] name of the file for the Content-Disposition header
# @param type[String] the content type (MIME type) of the archive being output
# @param use_chunked_transfer_encoding[Boolean] whether to forcibly encode output as chunked. Normally you should not need this.
# @param zip_streamer_options[Hash] options that will be passed to the Streamer.
# See {ZipKit::Streamer#initialize} for the full list of options.
# @yieldparam [ZipKit::Streamer] the streamer that can be written to
# @return [ZipKit::OutputEnumerator] The output enumerator assigned to the response body
# @return [Boolean] always returns true
def zip_kit_stream(filename: "download.zip", type: "application/zip", use_chunked_transfer_encoding: false, **zip_streamer_options, &zip_streaming_blk)
# We want some common headers for file sending. Rails will also set
# self.sending_file = true for us when we call send_file_headers!
Expand All @@ -30,19 +45,10 @@ def zip_kit_stream(filename: "download.zip", type: "application/zip", use_chunke

# The output enumerator yields chunks of bytes generated from the Streamer,
# with some buffering
output_enum = ZipKit::OutputEnumerator.new(**zip_streamer_options, &zip_streaming_blk)
rack_zip_body = ZipKit::OutputEnumerator.new(**zip_streamer_options, &zip_streaming_blk)

# Time for some branching, which mostly has to do with the 999 flavours of
# "how to make both Rails and Rack stream"
if self.class.ancestors.include?(ActionController::Live)
# If this controller includes Live it will not work correctly with a Rack
# response body assignment - we need to write into the Live output stream instead
begin
output_enum.each { |bytes| response.stream.write(bytes) }
ensure
response.stream.close
end
elsif use_chunked_transfer_encoding
# It is not really possible to force chunked encoding with ActionController::Live
if use_chunked_transfer_encoding
# Chunked encoding may be forced if, for example, you _need_ to bypass Rack::ContentLength.
# Rack::ContentLength is normally not in a Rails middleware stack, but it might get
# introduced unintentionally - for example, "rackup" adds the ContentLength middleware for you.
Expand All @@ -53,11 +59,25 @@ def zip_kit_stream(filename: "download.zip", type: "application/zip", use_chunke
# some especially pesky Rack middleware that just would not cooperate. Those include
# Rack::MiniProfiler and the above-mentioned Rack::ContentLength.
response.headers["Transfer-Encoding"] = "chunked"
self.response_body = ZipKit::RackChunkedBody.new(output_enum)
rack_zip_body = ZipKit::RackChunkedBody.new(rack_zip_body)
end

# Time for some branching, which mostly has to do with the 999 flavours of
# "how to make both Rails and Rack stream"
if self.class.ancestors.include?(ActionController::Live)
# If this controller includes Live it will not work correctly with a Rack
# response body assignment - the action will just hang. We need to read out the response
# body ourselves and write it into the Rails stream.
begin
rack_zip_body.each { |bytes| response.stream.write(bytes) }
ensure
response.stream.close
end
else
# Stream using a Rack body assigned to the ActionController response body, without
# doing explicit chunked encoding. See above for the reasoning.
self.response_body = output_enum
# Stream using a Rack body assigned to the ActionController response body
self.response_body = rack_zip_body
end

true
end
end
18 changes: 16 additions & 2 deletions rbi/zip_kit.rbi
Original file line number Diff line number Diff line change
Expand Up @@ -1979,6 +1979,20 @@ end, T.untyped)
# gets automatically forwarded to the Rails response stream. When the output completes,
# the Rails response stream is going to be closed automatically.
#
# Note that there is an important difference in how this method works, depending whether
# you use it in a controller which includes `ActionController::Live` vs. one that does not.
# With a standard `ActionController` this method will assign a response body, but streaming
# will begin when your action method returns. With `ActionController::Live` the streaming
# will begin immediately, before the method returns. In all other aspects the method should
# stream correctly in both types of controllers.
#
# If you encounter buffering (streaming does not start for a very long time) you probably
# have a piece of Rack middleware in your stack which buffers. Known offenders are `Rack::ContentLength`,
# `Rack::MiniProfiler` and `Rack::ETag`. ZipKit will try to work around these but it is not
# always possible. If you encounter buffering, examine your middleware stack and try to suss
# out whether any middleware might be buffering. You can also try setting `use_chunked_transfer_encoding`
# to `true` - this is not recommended but sometimes necessary, for example to bypass `Rack::ContentLength`.
#
# _@param_ `filename` — name of the file for the Content-Disposition header
#
# _@param_ `type` — the content type (MIME type) of the archive being output
Expand All @@ -1987,15 +2001,15 @@ end, T.untyped)
#
# _@param_ `zip_streamer_options` — options that will be passed to the Streamer. See {ZipKit::Streamer#initialize} for the full list of options.
#
# _@return_ — The output enumerator assigned to the response body
# _@return_ — always returns true
sig do
params(
filename: String,
type: String,
use_chunked_transfer_encoding: T::Boolean,
zip_streamer_options: T::Hash[T.untyped, T.untyped],
zip_streaming_blk: T.proc.params(the: ZipKit::Streamer).void
).returns(ZipKit::OutputEnumerator)
).returns(T::Boolean)
end
def zip_kit_stream(filename: "download.zip", type: "application/zip", use_chunked_transfer_encoding: false, **zip_streamer_options, &zip_streaming_blk); end
end
Expand Down

0 comments on commit 8892a7b

Please sign in to comment.