rack-streaming-proxy 1.0.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
-