├── .gitignore ├── lib ├── goliath-rack_proxy.rb └── goliath │ ├── rack_proxy │ └── rack_2_compatibility.rb │ └── rack_proxy.rb ├── Gemfile ├── Rakefile ├── test ├── test_helper.rb └── rack_proxy_test.rb ├── goliath-rack_proxy.gemspec ├── LICENSE.txt ├── CODE_OF_CONDUCT.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | pkg/ 3 | -------------------------------------------------------------------------------- /lib/goliath-rack_proxy.rb: -------------------------------------------------------------------------------- 1 | require "goliath/rack_proxy" 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "pry" 6 | gem "http", github: "janko-m/http", branch: "handle-early-responses" 7 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new do |t| 5 | t.libs << "test" 6 | t.test_files = FileList["test/**/*_test.rb"] 7 | t.warning = false 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /lib/goliath/rack_proxy/rack_2_compatibility.rb: -------------------------------------------------------------------------------- 1 | require "rack" 2 | 3 | # Async-rack attempts to require files that exist only in Rack 1.x even on Rack 4 | # 2.x, so we patch that behaviour to allow users to use this gem with Rack 2.x apps. 5 | module Kernel 6 | if Rack.release >= "2.0.0" 7 | alias original_rubygems_require require 8 | 9 | def require(file) 10 | case file 11 | when "rack/commonlogger" then original_rubygems_require("rack/common_logger") 12 | when "rack/conditionalget" then original_rubygems_require("rack/conditional_get") 13 | when "rack/showstatus" then original_rubygems_require("rack/show_status") 14 | else 15 | original_rubygems_require(file) 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | 3 | ENV["MT_NO_EXPECTATIONS"] = "1" 4 | 5 | require "minitest/autorun" 6 | require "minitest/pride" 7 | require "minitest/hooks/default" 8 | 9 | require "http" 10 | 11 | require "open3" 12 | require "tempfile" 13 | 14 | class Minitest::Test 15 | def start_server(ruby, args = []) 16 | tempfile = Tempfile.new 17 | tempfile << ruby 18 | tempfile.open 19 | 20 | command = %W[bundle exec ruby #{tempfile.path} --stdout] + args 21 | 22 | stdin, stdout, stderr, @thread = Open3.popen3(*command) 23 | 24 | HTTP.get("http://localhost:9000").to_s rescue retry 25 | 26 | Thread.new { IO.copy_stream(stderr, $stderr) } 27 | 28 | stdout 29 | end 30 | 31 | def stop_server 32 | if @thread 33 | Process.kill "TERM", @thread[:pid] 34 | @thread.join # wait for subprocess to finish 35 | @thread = nil 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /goliath-rack_proxy.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |gem| 2 | gem.name = "goliath-rack_proxy" 3 | gem.version = "1.1.0" 4 | 5 | gem.required_ruby_version = ">= 2.1" 6 | 7 | gem.summary = "Allows you to use Goliath as a web server for your Rack app, giving you streaming requests and responses." 8 | 9 | gem.homepage = "https://github.com/janko-m/goliath-rack_proxy" 10 | gem.authors = ["Janko Marohnić"] 11 | gem.email = ["janko.marohnic@gmail.com"] 12 | gem.license = "MIT" 13 | 14 | gem.files = Dir["README.md", "LICENSE.txt", "lib/**/*.rb", "*.gemspec"] 15 | gem.require_path = "lib" 16 | 17 | gem.add_dependency "goliath", ">= 1.0.6", "< 2" 18 | 19 | gem.add_development_dependency "rake", "~> 11.1" 20 | gem.add_development_dependency "minitest", "~> 5.8" 21 | gem.add_development_dependency "minitest-hooks" 22 | gem.add_development_dependency "http", "~> 3.0" 23 | gem.add_development_dependency "rack", "~> 2.0" 24 | end 25 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Janko Marohnić 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at janko.marohnic@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Goliath::RackProxy 2 | 3 | Allows you to use [Goliath] as a web server for your Rack app, giving you 4 | streaming requests and responses. 5 | 6 | ## DEPRECATED 7 | 8 | **This gem is deprecated in favor of [Falcon]. Falcon is a web server that 9 | utilizes non-blocking IO to process requests and responses in a streaming 10 | fashion without tying up web workers. It's built on top of the [async 11 | ecosystem], which is a promising alternative to EventMachine.** 12 | 13 | ## Motivation 14 | 15 | While developing [tus-ruby-server], a Rack application that handles large 16 | uploads and large downloads, I wanted to find an appropriate web server to 17 | recommend. I needed a web server that supports **streaming uploads**, allowing 18 | the Rack application to start processing the request while the request body is 19 | still being received, and that way giving it the ability to save whatever data 20 | it received before possible potential request interruption. I also needed 21 | support for **streaming downloads**, sending response body in chunks back to 22 | the client. 23 | 24 | The only web server I found that supported all of this was [Unicorn]. However, 25 | Unicorn needs to spawn a whole process for serving each concurrent request, 26 | which isn't the most efficent use of server resources. It's also difficult to 27 | estimate how many workers you need, because once you disable request buffering 28 | in the application server, you become vulnerable to slow clients. 29 | 30 | Then I came across [Goliath], which gave me the control I needed for handling 31 | requests. It's built on top of [EventMachine], which uses the reactor pattern 32 | to schedule work efficiently. The most important feature is that long-running 33 | requests won't impact request throughput, as there are no web workers that are 34 | waiting for incoming data. 35 | 36 | However, Goliath itself is designed to be used standalone, not in tandem with 37 | another Rack app. So I created `Goliath::RackProxy`, which is a `Goliath::API` 38 | subclass that proxies incoming/outgoing requests/responses to/from the 39 | specified Rack app in a streaming fashion, essentially making it act like a web 40 | server for the Rack app. 41 | 42 | ## Installation 43 | 44 | ```rb 45 | gem "goliath-rack_proxy" 46 | ``` 47 | 48 | ## Usage 49 | 50 | Create a file where you will initialize the Goliath Rack proxy: 51 | 52 | ```rb 53 | # app.rb 54 | require "goliath/rack_proxy" 55 | 56 | class MyGoliathApp < Goliath::RackProxy 57 | rack_app MyRackApp # provide your #call-able Rack application 58 | end 59 | ``` 60 | 61 | You can then run the server by running that Ruby file: 62 | 63 | ```sh 64 | $ ruby app.rb 65 | ``` 66 | 67 | Any command-line arguments passed after the file will be forwarded to the 68 | Goliath server (see [list of available options][goliath server options]): 69 | 70 | ```sh 71 | $ ruby app.rb --port 3000 --stdout 72 | ``` 73 | 74 | You can scale Goliath applications into multiple processes using [Einhorn]: 75 | 76 | ```sh 77 | $ einhorn -n COUNT -b 127.0.0.1:3000 ruby app.rb --einhorn 78 | ``` 79 | 80 | By default `Goliath::RackProxy` will use a rewindable `rack.input`, which means 81 | the data received from the client will be cached onto disk for the duration of 82 | the request. If you don't need the `rack.input` to be rewindable and want to 83 | save on disk I/O, you can disable caching: 84 | 85 | ```rb 86 | class MyGoliathApp < Goliath::RackProxy 87 | rack_app MyRackApp 88 | rewindable_input false 89 | end 90 | ``` 91 | 92 | ## License 93 | 94 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 95 | 96 | [Falcon]: https://github.com/socketry/falcon 97 | [async ecosystem]: https://github.com/socketry 98 | [Goliath]: https://github.com/postrank-labs/goliath 99 | [EventMachine]: https://github.com/eventmachine/eventmachine 100 | [tus-ruby-server]: https://github.com/janko-m/tus-ruby-server 101 | [Unicorn]: https://github.com/defunkt/unicorn 102 | [goliath server options]: https://github.com/postrank-labs/goliath/wiki/Server 103 | [Einhorn]: https://github.com/stripe/einhorn 104 | -------------------------------------------------------------------------------- /test/rack_proxy_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "http" 3 | require "time" 4 | require "timeout" 5 | 6 | describe "Goliath::RackProxy" do 7 | around do |&block| 8 | Timeout.timeout(10) do 9 | super(&block) 10 | end 11 | end 12 | 13 | after do 14 | stop_server 15 | end 16 | 17 | it "implements basic requests" do 18 | start_server <<~RUBY 19 | require "goliath/rack_proxy" 20 | 21 | class App < Goliath::RackProxy 22 | rack_app -> (env) { [200, {"Content-Length" => "5"}, ["Hello"]] } 23 | end 24 | RUBY 25 | 26 | response = HTTP.get("http://localhost:9000") 27 | 28 | assert_equal 200, response.status 29 | assert_equal "5", response.headers["Content-Length"] 30 | assert_equal "Goliath", response.headers["Server"] 31 | assert_equal "Hello", response.body.to_s 32 | end 33 | 34 | it "implements streaming uploads" do 35 | start_server <<~RUBY 36 | require "goliath/rack_proxy" 37 | 38 | class App < Goliath::RackProxy 39 | rack_app -> (env) { 40 | start_time = Time.now 41 | content = env["rack.input"].read 42 | [200, {"Read-Time" => (Time.now - start_time).to_s}, [content]] 43 | } 44 | end 45 | RUBY 46 | 47 | body = Enumerator.new { |y| y << "foo"; sleep 1; y << "bar" } 48 | 49 | response = HTTP 50 | .headers("Transfer-Encoding" => "chunked") 51 | .post("http://localhost:9000", body: body) 52 | 53 | assert_equal "foobar", response.body.to_s 54 | assert_in_delta 1, Float(response.headers["Read-Time"]), 0.1 55 | end 56 | 57 | it "gives rack input IO#read semantics" do 58 | start_server <<~RUBY 59 | require "goliath/rack_proxy" 60 | 61 | class App < Goliath::RackProxy 62 | rack_app -> (env) do 63 | body = [] 64 | body << env["rack.input"].read(3) 65 | body << env["rack.input"].read(2, "") 66 | body << env["rack.input"].read 67 | body << env["rack.input"].read(3) 68 | 69 | [200, {}, body.map(&:inspect)] 70 | end 71 | end 72 | RUBY 73 | 74 | response = HTTP 75 | .headers("Transfer-Encoding" => "chunked") 76 | .post("http://localhost:9000", body: ["he", "llo", " ", "world"]) 77 | 78 | assert_equal '"hel""lo"" world"nil', response.body.to_s 79 | end 80 | 81 | it "can rewind rewindable inputs" do 82 | start_server <<~RUBY 83 | require "goliath/rack_proxy" 84 | 85 | class App < Goliath::RackProxy 86 | rack_app -> (env) do 87 | env["rack.input"].read 88 | env["rack.input"].rewind 89 | [200, {}, [env["rack.input"].read]] 90 | end 91 | end 92 | RUBY 93 | 94 | response = HTTP 95 | .headers("Transfer-Encoding" => "chunked") 96 | .post("http://localhost:9000", body: ["foo", "bar", "baz"]) 97 | 98 | assert_equal "foobarbaz", response.body.to_s 99 | end 100 | 101 | it "cannot rewind non-rewindable inputs" do 102 | start_server <<~RUBY 103 | require "goliath/rack_proxy" 104 | 105 | class App < Goliath::RackProxy 106 | rack_app -> (env) { 107 | begin 108 | env["rack.input"].rewind 109 | rescue Errno::ESPIPE => exception 110 | [200, {}, [exception.inspect]] 111 | end 112 | } 113 | rewindable_input false 114 | end 115 | RUBY 116 | 117 | response = HTTP 118 | .headers("Transfer-Encoding" => "chunked") 119 | .post("http://localhost:9000", body: ["foo", "bar", "baz"]) 120 | 121 | assert_equal "#", response.body.to_s 122 | end 123 | 124 | it "implements streaming downloads" do 125 | start_server <<~RUBY 126 | require "goliath/rack_proxy" 127 | 128 | class App < Goliath::RackProxy 129 | rack_app -> (env) { 130 | body = Enumerator.new { |y| sleep 0.5; y << "a"*16*1024; sleep 0.5; y << "b"*16*1024 } 131 | [200, {"Content-Length" => (32*1024).to_s}, body] 132 | } 133 | end 134 | RUBY 135 | 136 | start_time = Time.now 137 | 138 | response = HTTP.get("http://localhost:9000") 139 | 140 | header_time = Time.now 141 | assert_equal (32*1024).to_s, response.headers["Content-Length"] 142 | assert_in_delta start_time, header_time, 0.2 143 | 144 | assert_equal "a"*16*1024, response.body.readpartial 145 | first_chunk_time = Time.now 146 | assert_in_delta header_time + 0.5, first_chunk_time, 0.2 147 | 148 | assert_equal "b"*16*1024, response.body.readpartial 149 | second_chunk_time = Time.now 150 | assert_in_delta first_chunk_time + 0.5, second_chunk_time, 0.2 151 | 152 | assert_nil response.body.readpartial 153 | assert_in_delta second_chunk_time, Time.now, 0.2 154 | end 155 | 156 | it "doesn't break when sent request body isn't read by the rack app" do 157 | start_server <<~RUBY 158 | require "goliath/rack_proxy" 159 | 160 | class App < Goliath::RackProxy 161 | rack_app -> (env) { [200, {"Content-Length" => "7"}, ["content"]] } 162 | end 163 | RUBY 164 | 165 | body = Enumerator.new { |y| sleep 0.5; y << "foo"; sleep 0.5; y << "bar" } 166 | 167 | response = HTTP 168 | .headers("Transfer-Encoding" => "chunked") 169 | .post("http://localhost:9000", body: body) 170 | 171 | assert_equal "content", response.body.to_s 172 | end 173 | 174 | it "prevents Sinatra from calling EM.defer when streaming the response" do 175 | start_server <<~RUBY 176 | require "goliath/rack_proxy" 177 | 178 | class App < Goliath::RackProxy 179 | rack_app -> (env) { 180 | body = Enumerator.new do |y| 181 | if env["async.callback"] 182 | EM.defer { y << "foo" } # this is what Sinatra essential does 183 | else 184 | y << "foo" 185 | end 186 | end 187 | 188 | [200, {"Content-Length" => "3"}, body] 189 | } 190 | end 191 | RUBY 192 | 193 | response = HTTP.get("http://localhost:9000") 194 | 195 | assert_equal "foo", response.body.to_s 196 | assert_equal "3", response.headers["Content-Length"] 197 | end 198 | end 199 | -------------------------------------------------------------------------------- /lib/goliath/rack_proxy.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | require "goliath/rack_proxy/rack_2_compatibility" 3 | require "goliath" 4 | require "tempfile" 5 | require "fiber" 6 | 7 | module Goliath 8 | class RackProxy < Goliath::API 9 | # Rack app to proxy the incoming requests to. 10 | def self.rack_app(app) 11 | rack_proxy_options[:rack_app] = app 12 | end 13 | 14 | # Whether the request body should be rewindable. 15 | def self.rewindable_input(value) 16 | rack_proxy_options[:rewindable_input] = value 17 | end 18 | 19 | # Custom user-defined options. 20 | def self.rack_proxy_options 21 | @rack_proxy_options ||= {} 22 | end 23 | 24 | # Starts the request to the given Rack application. 25 | def on_headers(env, headers) 26 | rack_app = self.class.rack_proxy_options.fetch(:rack_app) 27 | rewindable_input = self.class.rack_proxy_options.fetch(:rewindable_input, true) 28 | 29 | env["rack_proxy.call"] = RackCall.new(rack_app, env, rewindable_input: rewindable_input) 30 | env["rack_proxy.call"].resume(on_response: -> (response) { send_response(response, env) }) 31 | end 32 | 33 | # Resumes the Rack request with the received request body data. 34 | def on_body(env, data) 35 | env["rack_proxy.call"].resume(data, on_response: -> (response) { send_response(response, env) }) 36 | end 37 | 38 | # Resumes the Rack request with no more data. 39 | def on_close(env) 40 | env["rack_proxy.call"].resume 41 | end 42 | 43 | # Resumes the Rack request with no more data. 44 | def response(env) 45 | env["rack_proxy.call"].resume(on_response: -> (response) { send_response(response, env) }) 46 | nil 47 | end 48 | 49 | private 50 | 51 | # The env[ASYNC_CALLBACK] proc wraps sending response data in a 52 | # Goliath::Request#callback, which gets executed after the whole request 53 | # body has been received. 54 | # 55 | # This is not ideal for apps that receive large uploads, as when they 56 | # validate request headers, they likely want to return error responses 57 | # immediately. It's not good user experience to require the user to upload 58 | # a large file, only to have the request fail with a validation error. 59 | # 60 | # To work around that, we mark the request as succeeded before sending the 61 | # response, so that the response is sent immediately. 62 | def send_response(response, env) 63 | request = env[STREAM_SEND].binding.receiver # hack to get the Goliath::Request object 64 | request.succeed # makes it so that response is sent immediately 65 | 66 | env[ASYNC_CALLBACK].call(response) 67 | end 68 | 69 | # Allows "curry-calling" the Rack application, resuming the call as we're 70 | # receiving more request body data. 71 | class RackCall 72 | def initialize(app, env, rewindable_input: true) 73 | @app = app 74 | @env = env 75 | @rewindable_input = rewindable_input 76 | end 77 | 78 | def resume(data = nil, on_response: nil) 79 | if fiber.alive? 80 | response = fiber.resume(data) 81 | on_response.call(response) if response && on_response 82 | end 83 | end 84 | 85 | private 86 | 87 | # Calls the Rack application inside a Fiber, using the RackInput object as 88 | # the request body. When the Rack application wants to read request body 89 | # data that hasn't been received yet, the execution is automatically 90 | # paused so that the event loop can go on. 91 | def fiber 92 | @fiber ||= Fiber.new do 93 | rack_input = RackInput.new(rewindable: @rewindable_input) { Fiber.yield } 94 | 95 | response = @app.call @env.merge( 96 | "rack.input" => rack_input, 97 | "async.callback" => nil, # prevent Roda/Sinatra from calling EventMachine while streaming the response 98 | ) 99 | 100 | rack_input.close 101 | 102 | response 103 | end 104 | end 105 | end 106 | 107 | # IO-like object that conforms to the Rack specification for the request 108 | # body ("rack input"). It takes a block which produces chunks of data, and 109 | # makes this data retrievable through the IO#read interface. When rewindable 110 | # caches the retrieved content onto disk. 111 | class RackInput 112 | def initialize(rewindable: true, &next_chunk) 113 | @next_chunk = next_chunk 114 | @cache = Tempfile.new("goliath-rack_input", binmode: true) if rewindable 115 | @buffer = nil 116 | @eof = false 117 | end 118 | 119 | # Retrieves data using the IO#read semantics. If rack input is declared 120 | # rewindable, writes retrieved content into a Tempfile object so that 121 | # it can later be re-read. 122 | def read(length = nil, outbuf = nil) 123 | data = outbuf.clear if outbuf 124 | data = @cache.read(length, outbuf) if @cache && !@cache.eof? 125 | 126 | loop do 127 | remaining_length = length - data.bytesize if data && length 128 | 129 | break if remaining_length == 0 130 | 131 | @buffer = next_chunk or break if @buffer.nil? 132 | 133 | if remaining_length && remaining_length < @buffer.bytesize 134 | buffered_data = @buffer.byteslice(0, remaining_length) 135 | @buffer = @buffer.byteslice(remaining_length..-1) 136 | else 137 | buffered_data = @buffer 138 | @buffer = nil 139 | end 140 | 141 | if data 142 | data << buffered_data 143 | else 144 | data = buffered_data 145 | end 146 | 147 | @cache.write(buffered_data) if @cache 148 | 149 | buffered_data.clear unless data.equal?(buffered_data) 150 | end 151 | 152 | data.to_s unless length && (data.nil? || data.empty?) 153 | end 154 | 155 | # Rewinds the tempfile if rewindable. Otherwise raises Errno::ESPIPE 156 | # exception, which is what other non-rewindable Ruby IO objects raise. 157 | def rewind 158 | raise Errno::ESPIPE if @cache.nil? 159 | @cache.rewind 160 | end 161 | 162 | # Deletes the tempfile. The #close method is also part of the Rack 163 | # specification. 164 | def close 165 | @cache.close! if @cache 166 | end 167 | 168 | private 169 | 170 | # Retrieves the next chunk by calling the block, and marks EOF when nil 171 | # was returned. 172 | def next_chunk 173 | return if @eof 174 | chunk = @next_chunk.call 175 | @eof = true if chunk.nil? 176 | chunk 177 | end 178 | end 179 | end 180 | end 181 | --------------------------------------------------------------------------------