rack-streaming-proxy 1.0.3 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,14 @@
1
+ require 'rails/railtie'
2
+
3
+ class Rack::StreamingProxy::Railtie < Rails::Railtie
4
+
5
+ config.streaming_proxy = ActiveSupport::OrderedOptions.new
6
+
7
+ config.after_initialize do
8
+ options = config.streaming_proxy
9
+ Rack::StreamingProxy::Proxy.logger = options.logger if options.logger
10
+ Rack::StreamingProxy::Proxy.log_verbosity = options.log_verbosity if options.log_verbosity
11
+ Rack::StreamingProxy::Proxy.num_retries_on_5xx = options.num_retries_on_5xx if options.num_retries_on_5xx
12
+ Rack::StreamingProxy::Proxy.raise_on_5xx = options.raise_on_5xx if options.raise_on_5xx
13
+ end
14
+ end
@@ -0,0 +1,65 @@
1
+ require 'uri'
2
+ require 'net/https'
3
+
4
+ class Rack::StreamingProxy::Request
5
+
6
+ attr_reader :http_request
7
+
8
+ def initialize(destination_uri, current_request)
9
+ @destination_uri = URI.parse(destination_uri)
10
+ @http_request = translate_request(current_request, @destination_uri)
11
+ end
12
+
13
+ def host
14
+ @destination_uri.host
15
+ end
16
+
17
+ def port
18
+ @destination_uri.port
19
+ end
20
+
21
+ def use_ssl?
22
+ @destination_uri.is_a? URI::HTTPS
23
+ end
24
+
25
+ def uri
26
+ @destination_uri.to_s
27
+ end
28
+
29
+ private
30
+
31
+ def translate_request(current_request, uri)
32
+ method = current_request.request_method.downcase
33
+ method[0..0] = method[0..0].upcase
34
+
35
+ request = Net::HTTP.const_get(method).new("#{uri.path}#{"?" if uri.query}#{uri.query}")
36
+
37
+ if request.request_body_permitted? and current_request.body
38
+ request.body_stream = current_request.body
39
+ request.content_length = current_request.content_length if current_request.content_length
40
+ request.content_type = current_request.content_type if current_request.content_type
41
+ end
42
+
43
+ log_headers :debug, 'Current Request Headers', current_request.env
44
+
45
+ current_headers = current_request.env.reject { |key, value| !(key.match /^HTTP_/) }
46
+ current_headers.each do |key, value|
47
+ fixed_name = key.sub(/^HTTP_/, '').gsub('_', '-')
48
+ request[fixed_name] = value unless fixed_name.downcase == 'host'
49
+ end
50
+ request['X-Forwarded-For'] = (current_request.env['X-Forwarded-For'].to_s.split(/, +/) + [current_request.env['REMOTE_ADDR']]).join(', ')
51
+
52
+ log_headers :debug, 'Proxy Request Headers:', request
53
+
54
+ request
55
+ end
56
+
57
+ def log_headers(level, title, headers)
58
+ Rack::StreamingProxy::Proxy.log level, "+-------------------------------------------------------------"
59
+ Rack::StreamingProxy::Proxy.log level, "| #{title}"
60
+ Rack::StreamingProxy::Proxy.log level, "+-------------------------------------------------------------"
61
+ headers.each { |key, value| Rack::StreamingProxy::Proxy.log level, "| #{key} = #{value.to_s}" }
62
+ Rack::StreamingProxy::Proxy.log level, "+-------------------------------------------------------------"
63
+ end
64
+
65
+ end
@@ -0,0 +1,79 @@
1
+ require 'rack/streaming_proxy/errors'
2
+
3
+ class Rack::StreamingProxy::Response
4
+ include Rack::Utils # For HeaderHash
5
+
6
+ attr_reader :status, :headers
7
+ attr_accessor :client_http_version
8
+
9
+ def initialize(piper)
10
+ @piper = piper
11
+ @client_http_version = '1.1'
12
+ receive
13
+ end
14
+
15
+ # This method is called by Rack itself, to iterate over the proxied contents.
16
+ def each
17
+ if @body_permitted
18
+ term = "\r\n"
19
+
20
+ while chunk = read_from_destination
21
+ break if chunk == :done
22
+ if @chunked
23
+ size = bytesize(chunk)
24
+ next if size == 0
25
+ if @client_http_version >= '1.1'
26
+ yield [size.to_s(16), term, chunk, term].join
27
+ else
28
+ yield chunk
29
+ end
30
+ else
31
+ yield chunk
32
+ end
33
+ end
34
+
35
+ finish
36
+
37
+ if @chunked && @client_http_version >= '1.1'
38
+ yield ['0', term, '', term].join
39
+ end
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def receive
46
+ # The first item received from the child will either be an HTTP status code or an Exception.
47
+ @status = read_from_destination
48
+
49
+ if @status.nil? # This should never happen
50
+ Rack::StreamingProxy::Proxy.log :error, "Parent received unexpected nil status!"
51
+ finish
52
+ raise Rack::StreamingProxy::UnknownError
53
+ elsif @status.kind_of? Exception
54
+ e = @status
55
+ Rack::StreamingProxy::Proxy.log :error, "Parent received an Exception from Child: #{e.class}: #{e.message}"
56
+ finish
57
+ raise e
58
+ end
59
+
60
+ Rack::StreamingProxy::Proxy.log :debug, "Parent received: Status = #{@status}."
61
+ @body_permitted = read_from_destination
62
+ Rack::StreamingProxy::Proxy.log :debug, "Parent received: Reponse has body? = #{@body_permitted}."
63
+ @headers = HeaderHash.new(read_from_destination)
64
+ @chunked = (@headers['Transfer-Encoding'] == 'chunked')
65
+ finish unless @body_permitted # If there is a body, finish will be called inside each.
66
+ end
67
+
68
+ # parent needs to wait for the child, or it results in the child process becoming defunct, resulting in zombie processes!
69
+ # This is very important. See: http://siliconisland.ca/2013/04/26/beware-of-the-zombie-process-apocalypse/
70
+ def finish
71
+ Rack::StreamingProxy::Proxy.log :info, "Parent process #{Process.pid} waiting for child process #{@piper.pid} to exit."
72
+ @piper.wait
73
+ end
74
+
75
+ def read_from_destination
76
+ @piper.gets
77
+ end
78
+
79
+ end
@@ -0,0 +1,119 @@
1
+ require 'uri'
2
+ require 'net/https'
3
+ require 'servolux'
4
+ require 'rack/streaming_proxy/errors'
5
+
6
+ class Rack::StreamingProxy::Session
7
+
8
+ def initialize(request)
9
+ @request = request
10
+ end
11
+
12
+ # Returns a Rack::StreamingProxy::Response
13
+ def start
14
+ @piper = Servolux::Piper.new 'r', timeout: 30
15
+ @piper.child { child }
16
+ @piper.parent { parent }
17
+ end
18
+
19
+ private
20
+
21
+ def child
22
+ begin
23
+ Rack::StreamingProxy::Proxy.log :debug, "Child starting request to #{@request.uri}"
24
+ perform_request
25
+
26
+ rescue Exception => e
27
+ # Rescue all exceptions to help with development and debugging, as otherwise when exceptions
28
+ # occur the child process doesn't crash the parent process. Normally rescuing from Exception is a bad idea,
29
+ # but it's the only way to get a stacktrace here for all exceptions including SyntaxError etc,
30
+ # and we are simply passing it on so catastrophic exceptions will still be raised up the chain.
31
+ Rack::StreamingProxy::Proxy.log :debug, "Child process #{Process.pid} passing on #{e.class}: #{e.message}"
32
+ @piper.puts e # Pass on the exception to the parent.
33
+
34
+ ensure
35
+ Rack::StreamingProxy::Proxy.log :debug, "Child process #{Process.pid} closing connection."
36
+ @piper.close
37
+
38
+ Rack::StreamingProxy::Proxy.log :info, "Child process #{Process.pid} exiting."
39
+ exit!(0) # child needs to exit, always.
40
+ end
41
+ end
42
+
43
+ def parent
44
+ Rack::StreamingProxy::Proxy.log :info, "Parent process #{Process.pid} forked a child process #{@piper.pid}."
45
+
46
+ response = Rack::StreamingProxy::Response.new(@piper)
47
+ return response
48
+ end
49
+
50
+ def perform_request
51
+ http_session = Net::HTTP.new(@request.host, @request.port)
52
+ http_session.use_ssl = @request.use_ssl?
53
+
54
+ http_session.start do |session|
55
+ # Retry the request up to self.class.num_retries_on_5xx times if a 5xx is experienced.
56
+ # This is because some 500/503 errors resolve themselves quickly, might as well give it a chance.
57
+ # do...while loop as suggested by Matz: http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-core/6745
58
+ retries = 1
59
+ stop = false
60
+ loop do
61
+ session.request(@request.http_request) do |response|
62
+ # At this point the headers and status are available, but the body has not yet been read.
63
+ Rack::StreamingProxy::Proxy.log :debug, "Child got response: #{response.class.name}"
64
+
65
+ if response.class <= Net::HTTPServerError # Includes Net::HTTPServiceUnavailable, Net::HTTPInternalServerError
66
+ if retries <= Rack::StreamingProxy::Proxy.num_retries_on_5xx
67
+ Rack::StreamingProxy::Proxy.log :info, "Child got #{response.code}, retrying (Retry ##{retries})"
68
+ sleep 1
69
+ retries += 1
70
+ next
71
+ end
72
+ end
73
+ stop = true
74
+
75
+ Rack::StreamingProxy::Proxy.log :debug, "Child process #{Process.pid} returning Status = #{response.code}."
76
+
77
+ process_response(response)
78
+ end
79
+
80
+ break if stop
81
+ end
82
+ end
83
+ end
84
+
85
+ def process_response(response)
86
+
87
+ # Raise an exception if the raise_on_5xx config is set, and the response is a 5xx.
88
+ # Otherwise continue and put the error body in the pipe. (e.g. Apache error page, for example)
89
+ if response.class <= Net::HTTPServerError && Rack::StreamingProxy::Proxy.raise_on_5xx
90
+ raise Rack::StreamingProxy::HttpServerError.new "Got a #{response.class.name} (#{response.code}) response while proxying to #{@request.uri}"
91
+ end
92
+
93
+ # Put the response in the parent's pipe.
94
+ @piper.puts response.code
95
+ @piper.puts response.class.body_permitted?
96
+
97
+ # Could potentially use a one-liner here:
98
+ # @piper.puts Hash[response.to_hash.map { |key, value| [key, value.join(', ')] } ]
99
+ # But the following three lines seem to be more readable.
100
+ # Watch out: response.to_hash and response.each_header returns in different formats!
101
+ # to_hash requires the values to be joined with a comma.
102
+ headers = {}
103
+ response.each_header { |key, value| headers[key] = value }
104
+ log_headers :debug, 'Proxy Response Headers:', headers
105
+ @piper.puts headers
106
+
107
+ response.read_body { |chunk| @piper.puts chunk }
108
+ @piper.puts :done
109
+ end
110
+
111
+ def log_headers(level, title, headers)
112
+ Rack::StreamingProxy::Proxy.log level, "+-------------------------------------------------------------"
113
+ Rack::StreamingProxy::Proxy.log level, "| #{title}"
114
+ Rack::StreamingProxy::Proxy.log level, "+-------------------------------------------------------------"
115
+ headers.each { |key, value| Rack::StreamingProxy::Proxy.log level, "| #{key} = #{value.to_s}" }
116
+ Rack::StreamingProxy::Proxy.log level, "+-------------------------------------------------------------"
117
+ end
118
+
119
+ end
@@ -0,0 +1,5 @@
1
+ module Rack
2
+ module StreamingProxy
3
+ VERSION = "2.0.0"
4
+ end
5
+ end
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'rack/streaming_proxy/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'rack-streaming-proxy'
8
+ spec.version = Rack::StreamingProxy::VERSION
9
+ spec.authors = ['Fred Ngo', 'Nathan Witmer']
10
+ spec.email = ['fredngo@gmail.com', 'nwitmer@gmail.com']
11
+ spec.description = %q{Streaming proxy for Rack, the rainbows to Rack::Proxy's unicorn.}
12
+ spec.summary = %q{Streaming proxy for Rack, the rainbows to Rack::Proxy's unicorn.}
13
+ spec.homepage = 'http://github.com/fredngo/rack-streaming-proxy'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_runtime_dependency 'rack', '>= 1.4'
22
+ spec.add_runtime_dependency 'servolux', '~> 0.10'
23
+
24
+ # test
25
+ spec.add_development_dependency 'bundler', '>= 1.3'
26
+ spec.add_development_dependency 'rake', '>= 10.0'
27
+ spec.add_development_dependency 'rspec'
28
+ spec.add_development_dependency 'rack-test'
29
+ spec.add_development_dependency 'unicorn'
30
+
31
+ # debug
32
+ spec.add_development_dependency 'pry'
33
+ spec.add_development_dependency 'pry-nav'
34
+ end
data/spec/app.ru CHANGED
@@ -31,7 +31,7 @@ end
31
31
  use Rack::ContentLength
32
32
 
33
33
  map "/" do
34
- run lambda { |env| [200, {"Content-Type" => "text/plain"}, "ALL GOOD"] }
34
+ run lambda { |env| [200, {"Content-Type" => "text/plain"}, ["ALL GOOD"]] }
35
35
  end
36
36
 
37
37
  map "/stream" do
@@ -50,6 +50,6 @@ map "/env" do
50
50
  end
51
51
 
52
52
  map "/boom" do
53
- run lambda { |env| [500, {"Content-Type" => "text/plain"}, "kaboom!"] }
53
+ run lambda { |env| [500, {"Content-Type" => "text/plain"}, ["kaboom!"]] }
54
54
  end
55
55
 
data/spec/proxy.ru CHANGED
@@ -1,9 +1,9 @@
1
1
  require File.expand_path(
2
2
  File.join(File.dirname(__FILE__), %w[.. lib rack streaming_proxy]))
3
3
 
4
- use Rack::Lint
4
+ ENV['RACK_ENV'] = 'none' # 'development' automatically use Rack::Lint and results in errors with unicorn
5
5
  # use Rack::CommonLogger
6
- use Rack::StreamingProxy do |req|
6
+ use Rack::StreamingProxy::Proxy do |req|
7
7
  "http://localhost:4321#{req.path}"
8
8
  end
9
- run lambda { |env| [200, {}, "should never get here..."]}
9
+ run lambda { |env| [200, {}, ["should never get here..."]]}
data/spec/spec_helper.rb CHANGED
@@ -1,8 +1,9 @@
1
- require File.expand_path(
2
- File.join(File.dirname(__FILE__), %w[.. lib rack streaming_proxy]))
1
+ require File.expand_path( File.join(File.dirname(__FILE__), %w[.. lib rack streaming_proxy]))
3
2
 
4
3
  require "rack/test"
5
4
 
6
- Spec::Runner.configure do |config|
5
+ RSpec.configure do |config|
6
+ config.treat_symbols_as_metadata_keys_with_true_values = true
7
+ config.run_all_when_everything_filtered = true
8
+ config.filter_run :focus
7
9
  end
8
-
@@ -1,15 +1,85 @@
1
+ require 'yaml'
1
2
  require File.join(File.dirname(__FILE__), %w[spec_helper])
2
3
 
3
- describe Rack::StreamingProxy do
4
- include Rack::Test::Methods
4
+ APP_PORT = 4321 # hardcoded in proxy.ru as well!
5
+ PROXY_PORT = 4322
5
6
 
6
- APP_PORT = 4321 # hardcoded in proxy.ru as well!
7
- PROXY_PORT = 4322
7
+ shared_examples "rack-streaming-proxy" do
8
+ it "passes through to the rest of the stack if block returns false" do
9
+ get "/not_proxied"
10
+ last_response.should be_ok
11
+ last_response.body.should == "not proxied"
12
+ end
13
+
14
+ it "proxies a request back to the app server" do
15
+ get "/", {}, rack_env
16
+ last_response.should be_ok
17
+ last_response.body.should == "ALL GOOD"
18
+ # Expect a Content-Length header field which the origin server sent is
19
+ # not deleted by streaming-proxy.
20
+ last_response.headers["Content-Length"].should eq '8'
21
+ end
22
+
23
+ it "handles POST, PUT, and DELETE methods" do
24
+ post "/env", {}, rack_env
25
+ last_response.should be_ok
26
+ last_response.body.should =~ /REQUEST_METHOD: POST/
27
+ put "/env", {}, rack_env
28
+ last_response.should be_ok
29
+ last_response.body.should =~ /REQUEST_METHOD: PUT/
30
+ delete "/env", {}, rack_env
31
+ last_response.should be_ok
32
+ last_response.body.should =~ /REQUEST_METHOD: DELETE/
33
+ end
34
+
35
+ it "sets a X-Forwarded-For header" do
36
+ post "/env", {}, rack_env
37
+ last_response.should =~ /HTTP_X_FORWARDED_FOR: 127.0.0.1/
38
+ end
39
+
40
+ it "preserves the post body" do
41
+ post "/env", {"foo" => "bar"}, rack_env
42
+ last_response.body.should =~ /rack.request.form_vars: foo=bar/
43
+ end
44
+
45
+ it "raises a Rack::Proxy::StreamingProxy error when something goes wrong" do
46
+ Rack::StreamingProxy::Request.should_receive(:new).and_raise(RuntimeError.new("kaboom"))
47
+ lambda { get "/" }.should raise_error(RuntimeError, /kaboom/i)
48
+ end
49
+
50
+ it "does not raise a Rack::Proxy error if the app itself raises something" do
51
+ lambda { get "/not_proxied/boom" }.should raise_error(RuntimeError, /app error/)
52
+ end
53
+
54
+ it "preserves cookies" do
55
+ set_cookie "foo"
56
+ post "/env", {}, rack_env
57
+ YAML::load(last_response.body)["HTTP_COOKIE"].should == "foo"
58
+ end
59
+
60
+ it "preserves authentication info" do
61
+ basic_authorize "admin", "secret"
62
+ post "/env", {}, rack_env
63
+ YAML::load(last_response.body)["HTTP_AUTHORIZATION"].should == "Basic YWRtaW46c2VjcmV0"
64
+ end
65
+
66
+ it "preserves arbitrary headers" do
67
+ get "/env", {}, rack_env.merge("HTTP_X_FOOHEADER" => "Bar")
68
+ YAML::load(last_response.body)["HTTP_X_FOOHEADER"].should == "Bar"
69
+ end
70
+ end
71
+
72
+ describe Rack::StreamingProxy::Proxy do
73
+ include Rack::Test::Methods
8
74
 
9
75
  def app
10
76
  @app ||= Rack::Builder.new do
11
77
  use Rack::Lint
12
- use Rack::StreamingProxy do |req|
78
+ use Rack::StreamingProxy::Proxy do |req|
79
+ # STDERR.puts "== incoming request env =="
80
+ # STDERR.puts req.env
81
+ # STDERR.puts "=^ incoming request env ^="
82
+ # STDERR.puts
13
83
  unless req.path.start_with?("/not_proxied")
14
84
  url = "http://localhost:#{APP_PORT}#{req.path}"
15
85
  url << "?#{req.query_string}" unless req.query_string.empty?
@@ -19,13 +89,13 @@ describe Rack::StreamingProxy do
19
89
  end
20
90
  run lambda { |env|
21
91
  raise "app error" if env["PATH_INFO"] =~ /boom/
22
- [200, {"Content-Type" => "text/plain"}, "not proxied"]
92
+ [200, {"Content-Type" => "text/plain"}, ["not proxied"]]
23
93
  }
24
94
  end
25
95
  end
26
96
 
27
97
  before(:all) do
28
- app_path = Rack::StreamingProxy.path("spec", "app.ru")
98
+ app_path = File.join(File.dirname(__FILE__), %w[app.ru])
29
99
  @app_server = Servolux::Child.new(
30
100
  # :command => "thin -R #{app_path} -p #{APP_PORT} start", # buffers!
31
101
  :command => "rackup #{app_path} -p #{APP_PORT}",
@@ -45,10 +115,76 @@ describe Rack::StreamingProxy do
45
115
  puts "----- app server is stopped -----"
46
116
  end
47
117
 
118
+ context 'client requests with HTTP/1.0' do
119
+ let(:rack_env) { {'HTTP_VERSION' => 'HTTP/1.0'} }
120
+ it_behaves_like 'rack-streaming-proxy'
121
+ it "does not use chunked encoding when the app server send chunked body" do
122
+ get "/stream", {}, rack_env
123
+ last_response.should be_ok
124
+ # Expect a Transfer-Encoding header is deleted by rack-streaming-proxy
125
+ last_response.headers["Transfer-Encoding"].should be_nil
126
+ # I expected a Content-Length header which the origin server sent was deleted,
127
+ # But the following test failed against my expectation. The reason is
128
+ # that a Content-Length header was added in creating Rack::MockResponse
129
+ # instance. So I gave up writing this test right now.
130
+ #
131
+ # last_response.headers["Content-Length"].should be_nil
132
+ #
133
+ last_response.body.should == <<-EOS
134
+ ~~~~~ 0 ~~~~~
135
+ ~~~~~ 1 ~~~~~
136
+ ~~~~~ 2 ~~~~~
137
+ ~~~~~ 3 ~~~~~
138
+ ~~~~~ 4 ~~~~~
139
+ EOS
140
+ end
141
+ end
142
+
143
+ context 'client requests with HTTP/1.1' do
144
+ let(:rack_env) { {'HTTP_VERSION' => 'HTTP/1.1'} }
145
+ it_behaves_like 'rack-streaming-proxy'
146
+ it "uses chunked encoding when the app server send chunked body" do
147
+ get "/stream", {}, rack_env
148
+ last_response.should be_ok
149
+ last_response.headers["Transfer-Encoding"].should == 'chunked'
150
+ last_response.headers["Content-Length"].should be_nil
151
+ last_response.body.should =~ /^e\r\n~~~~~ 0 ~~~~~\n\r\n/
152
+ end
153
+ end
154
+
155
+ end
156
+
157
+ describe Rack::StreamingProxy::Proxy do
158
+ include Rack::Test::Methods
159
+
160
+ attr_reader :app
161
+
162
+ before(:all) do
163
+ app_path = File.join(File.dirname(__FILE__), %w[app.ru])
164
+ @app_server = Servolux::Child.new(
165
+ # :command => "thin -R #{app_path} -p #{APP_PORT} start", # buffers!
166
+ # :command => "rackup #{app_path} -p #{APP_PORT}", # webrick adds content-length, it should be wrong
167
+ :command => "unicorn #{app_path} -p #{APP_PORT} -E none",
168
+ :timeout => 30, # all specs should take <30 sec to run
169
+ :suspend => 0.25
170
+ )
171
+ puts "----- starting app server -----"
172
+ @app_server.start
173
+ sleep 2 # give it a sec
174
+ puts "----- started app server -----"
175
+ end
176
+
177
+ after(:all) do
178
+ puts "----- shutting down app server -----"
179
+ @app_server.stop
180
+ @app_server.wait
181
+ puts "----- app server is stopped -----"
182
+ end
183
+
48
184
  def with_proxy_server
49
- proxy_path = Rack::StreamingProxy.path("spec", "proxy.ru")
185
+ proxy_path = File.join(File.dirname(__FILE__), %w[proxy.ru])
50
186
  @proxy_server = Servolux::Child.new(
51
- :command => "rackup #{proxy_path} -p #{PROXY_PORT}",
187
+ :command => "unicorn #{proxy_path} -p #{PROXY_PORT} -E none",
52
188
  :timeout => 10,
53
189
  :suspend => 0.25
54
190
  )
@@ -64,26 +200,8 @@ describe Rack::StreamingProxy do
64
200
  puts "----- proxy server is stopped -----"
65
201
  end
66
202
 
67
- it "passes through to the rest of the stack if block returns false" do
68
- get "/not_proxied"
69
- last_response.should be_ok
70
- last_response.body.should == "not proxied"
71
- end
72
-
73
- it "proxies a request back to the app server" do
74
- get "/"
75
- last_response.should be_ok
76
- last_response.body.should == "ALL GOOD"
77
- end
78
-
79
- it "uses chunked encoding when the app server send data that way" do
80
- get "/stream"
81
- last_response.should be_ok
82
- last_response.headers["Transfer-Encoding"].should == "chunked"
83
- last_response.body.should =~ /^e\r\n~~~~~ 0 ~~~~~\n\r\n/
84
- end
85
-
86
203
  # this is the most critical spec: it makes sure things are actually streamed, not buffered
204
+ # MEMO: only unicorn worked. webrick, thin, and puma did not progressively stream
87
205
  it "streams data from the app server to the client" do
88
206
  @app = Rack::Builder.new do
89
207
  use Rack::Lint
@@ -105,55 +223,8 @@ describe Rack::StreamingProxy do
105
223
  last_response.should be_ok
106
224
  times = last_response.body.split("\n").map {|l| l.to_i}
107
225
  unless (times.last - times.first) >= 2
108
- violated "expected receive time of first chunk to be at least " +
109
- "two seconds before the last chunk"
226
+ fail "expected receive time of first chunk to be at least two seconds before the last chunk, but the times were: #{times.join(', ')}"
110
227
  end
111
228
  end
112
-
113
- end
114
-
115
- it "handles POST, PUT, and DELETE methods" do
116
- post "/env"
117
- last_response.should be_ok
118
- last_response.body.should =~ /REQUEST_METHOD: POST/
119
- put "/env"
120
- last_response.should be_ok
121
- last_response.body.should =~ /REQUEST_METHOD: PUT/
122
- delete "/env"
123
- last_response.should be_ok
124
- last_response.body.should =~ /REQUEST_METHOD: DELETE/
125
- end
126
-
127
- it "sets a X-Forwarded-For header" do
128
- post "/env"
129
- last_response.should =~ /HTTP_X_FORWARDED_FOR: 127.0.0.1/
130
- end
131
-
132
- it "preserves the post body" do
133
- post "/env", "foo" => "bar"
134
- last_response.body.should =~ /rack.request.form_vars: foo=bar/
135
- end
136
-
137
- it "raises a Rack::Proxy::StreamingProxy error when something goes wrong" do
138
- Rack::StreamingProxy::ProxyRequest.should_receive(:new).and_raise(RuntimeError.new("kaboom"))
139
- lambda { get "/" }.should raise_error(Rack::StreamingProxy::Error, /proxy error.*kaboom/i)
140
- end
141
-
142
- it "does not raise a Rack::Proxy error if the app itself raises something" do
143
- lambda { get "/not_proxied/boom" }.should raise_error(RuntimeError, /app error/)
144
- end
145
-
146
- it "preserves cookies" do
147
- set_cookie "foo"
148
- post "/env"
149
- YAML.load(last_response.body)["HTTP_COOKIE"].should == "foo"
150
- end
151
-
152
- it "preserves authentication info" do
153
- basic_authorize "admin", "secret"
154
- post "/env"
155
- YAML.load(last_response.body)["HTTP_AUTHORIZATION"].should == "Basic YWRtaW46c2VjcmV0\n"
156
229
  end
157
-
158
230
  end
159
-