test_server: test_early_hints: fix test reliability
[unicorn.git] / test / unit / test_server.rb
blob384fa6bb7cf3a0abd2d57f2ba1729df65fa6cd54
1 # -*- encoding: binary -*-
3 # Copyright (c) 2005 Zed A. Shaw
4 # You can redistribute it and/or modify it under the same terms as Ruby 1.8 or
5 # the GPLv2+ (GPLv3+ preferred)
7 # Additional work donated by contributors.  See git history
8 # for more information.
10 require './test/test_helper'
12 include Unicorn
14 class TestHandler
16   def call(env)
17     while env['rack.input'].read(4096)
18     end
19     [200, { 'Content-Type' => 'text/plain' }, ['hello!\n']]
20   rescue Unicorn::ClientShutdown, Unicorn::HttpParserError => e
21     $stderr.syswrite("#{e.class}: #{e.message} #{e.backtrace.empty?}\n")
22     raise e
23   end
24 end
26 class TestEarlyHintsHandler
27   def call(env)
28     while env['rack.input'].read(4096)
29     end
30     env['rack.early_hints'].call(
31       "Link" => "</style.css>; rel=preload; as=style\n</script.js>; rel=preload"
32     )
33     [200, { 'Content-Type' => 'text/plain' }, ['hello!\n']]
34   end
35 end
37 class WebServerTest < Test::Unit::TestCase
39   def setup
40     @valid_request = "GET / HTTP/1.1\r\nHost: www.zedshaw.com\r\nContent-Type: text/plain\r\n\r\n"
41     @port = unused_port
42     @tester = TestHandler.new
43     redirect_test_io do
44       @server = HttpServer.new(@tester, :listeners => [ "127.0.0.1:#{@port}" ] )
45       @server.start
46     end
47   end
49   def teardown
50     redirect_test_io do
51       wait_workers_ready("test_stderr.#$$.log", 1)
52       File.truncate("test_stderr.#$$.log", 0)
53       @server.stop(false)
54     end
55     reset_sig_handlers
56   end
58   def test_preload_app_config
59     teardown
60     tmp = Tempfile.new('test_preload_app_config')
61     ObjectSpace.undefine_finalizer(tmp)
62     app = lambda { ||
63       tmp.sysseek(0)
64       tmp.truncate(0)
65       tmp.syswrite($$)
66       lambda { |env| [ 200, { 'Content-Type' => 'text/plain' }, [ "#$$\n" ] ] }
67     }
68     redirect_test_io do
69       @server = HttpServer.new(app, :listeners => [ "127.0.0.1:#@port"] )
70       @server.start
71     end
72     results = hit(["http://localhost:#@port/"])
73     worker_pid = results[0].to_i
74     assert worker_pid != 0
75     tmp.sysseek(0)
76     loader_pid = tmp.sysread(4096).to_i
77     assert loader_pid != 0
78     assert_equal worker_pid, loader_pid
79     teardown
81     redirect_test_io do
82       @server = HttpServer.new(app, :listeners => [ "127.0.0.1:#@port"],
83                                :preload_app => true)
84       @server.start
85     end
86     results = hit(["http://localhost:#@port/"])
87     worker_pid = results[0].to_i
88     assert worker_pid != 0
89     tmp.sysseek(0)
90     loader_pid = tmp.sysread(4096).to_i
91     assert_equal $$, loader_pid
92     assert worker_pid != loader_pid
93   ensure
94     tmp.close!
95   end
97   def test_early_hints
98     teardown
99     redirect_test_io do
100       @server = HttpServer.new(TestEarlyHintsHandler.new,
101                                :listeners => [ "127.0.0.1:#@port"],
102                                :early_hints => true)
103       @server.start
104     end
106     sock = TCPSocket.new('127.0.0.1', @port)
107     sock.syswrite("GET / HTTP/1.0\r\n\r\n")
109     responses = sock.read(4096)
110     assert_match %r{\AHTTP/1.[01] 103\b}, responses
111     assert_match %r{^Link: </style\.css>}, responses
112     assert_match %r{^Link: </script\.js>}, responses
114     assert_match %r{^HTTP/1.[01] 200\b}, responses
115   end
117   def test_broken_app
118     teardown
119     app = lambda { |env| raise RuntimeError, "hello" }
120     # [200, {}, []] }
121     redirect_test_io do
122       @server = HttpServer.new(app, :listeners => [ "127.0.0.1:#@port"] )
123       @server.start
124     end
125     sock = TCPSocket.new('127.0.0.1', @port)
126     sock.syswrite("GET / HTTP/1.0\r\n\r\n")
127     assert_match %r{\AHTTP/1.[01] 500\b}, sock.sysread(4096)
128     assert_nil sock.close
129   end
131   def test_simple_server
132     results = hit(["http://localhost:#{@port}/test"])
133     assert_equal 'hello!\n', results[0], "Handler didn't really run"
134   end
136   def test_client_shutdown_writes
137     bs = 15609315 * rand
138     sock = TCPSocket.new('127.0.0.1', @port)
139     sock.syswrite("PUT /hello HTTP/1.1\r\n")
140     sock.syswrite("Host: example.com\r\n")
141     sock.syswrite("Transfer-Encoding: chunked\r\n")
142     sock.syswrite("Trailer: X-Foo\r\n")
143     sock.syswrite("\r\n")
144     sock.syswrite("%x\r\n" % [ bs ])
145     sock.syswrite("F" * bs)
146     sock.syswrite("\r\n0\r\nX-")
147     "Foo: bar\r\n\r\n".each_byte do |x|
148       sock.syswrite x.chr
149       sleep 0.05
150     end
151     # we wrote the entire request before shutting down, server should
152     # continue to process our request and never hit EOFError on our sock
153     sock.shutdown(Socket::SHUT_WR)
154     buf = sock.read
155     assert_equal 'hello!\n', buf.split(/\r\n\r\n/).last
156     next_client = Net::HTTP.get(URI.parse("http://127.0.0.1:#@port/"))
157     assert_equal 'hello!\n', next_client
158     lines = File.readlines("test_stderr.#$$.log")
159     assert lines.grep(/^Unicorn::ClientShutdown: /).empty?
160     assert_nil sock.close
161   end
163   def test_client_shutdown_write_truncates
164     bs = 15609315 * rand
165     sock = TCPSocket.new('127.0.0.1', @port)
166     sock.syswrite("PUT /hello HTTP/1.1\r\n")
167     sock.syswrite("Host: example.com\r\n")
168     sock.syswrite("Transfer-Encoding: chunked\r\n")
169     sock.syswrite("Trailer: X-Foo\r\n")
170     sock.syswrite("\r\n")
171     sock.syswrite("%x\r\n" % [ bs ])
172     sock.syswrite("F" * (bs / 2.0))
174     # shutdown prematurely, this will force the server to abort
175     # processing on us even during app dispatch
176     sock.shutdown(Socket::SHUT_WR)
177     IO.select([sock], nil, nil, 60) or raise "Timed out"
178     buf = sock.read
179     assert_equal "", buf
180     next_client = Net::HTTP.get(URI.parse("http://127.0.0.1:#@port/"))
181     assert_equal 'hello!\n', next_client
182     lines = File.readlines("test_stderr.#$$.log")
183     lines = lines.grep(/^Unicorn::ClientShutdown: bytes_read=\d+/)
184     assert_equal 1, lines.size
185     assert_match %r{\AUnicorn::ClientShutdown: bytes_read=\d+ true$}, lines[0]
186     assert_nil sock.close
187   end
189   def test_client_malformed_body
190     bs = 15653984
191     sock = TCPSocket.new('127.0.0.1', @port)
192     sock.syswrite("PUT /hello HTTP/1.1\r\n")
193     sock.syswrite("Host: example.com\r\n")
194     sock.syswrite("Transfer-Encoding: chunked\r\n")
195     sock.syswrite("Trailer: X-Foo\r\n")
196     sock.syswrite("\r\n")
197     sock.syswrite("%x\r\n" % [ bs ])
198     sock.syswrite("F" * bs)
199     begin
200       File.open("/dev/urandom", "rb") { |fp| sock.syswrite(fp.sysread(16384)) }
201     rescue
202     end
203     assert_nil sock.close
204     next_client = Net::HTTP.get(URI.parse("http://127.0.0.1:#@port/"))
205     assert_equal 'hello!\n', next_client
206     lines = File.readlines("test_stderr.#$$.log")
207     lines = lines.grep(/^Unicorn::HttpParserError: .* true$/)
208     assert_equal 1, lines.size
209   end
211   def do_test(string, chunk, close_after=nil, shutdown_delay=0)
212     # Do not use instance variables here, because it needs to be thread safe
213     socket = TCPSocket.new("127.0.0.1", @port);
214     request = StringIO.new(string)
215     chunks_out = 0
217     while data = request.read(chunk)
218       chunks_out += socket.write(data)
219       socket.flush
220       sleep 0.2
221       if close_after and chunks_out > close_after
222         socket.close
223         sleep 1
224       end
225     end
226     sleep(shutdown_delay)
227     socket.write(" ") # Some platforms only raise the exception on attempted write
228     socket.flush
229   end
231   def test_trickle_attack
232     do_test(@valid_request, 3)
233   end
235   def test_close_client
236     assert_raises IOError do
237       do_test(@valid_request, 10, 20)
238     end
239   end
241   def test_bad_client
242     redirect_test_io do
243       do_test("GET /test HTTP/BAD", 3)
244     end
245   end
247   def test_logger_set
248     assert_equal @server.logger, Unicorn::HttpRequest::DEFAULTS["rack.logger"]
249   end
251   def test_logger_changed
252     tmp = Logger.new($stdout)
253     @server.logger = tmp
254     assert_equal tmp, Unicorn::HttpRequest::DEFAULTS["rack.logger"]
255   end
257   def test_bad_client_400
258     sock = TCPSocket.new('127.0.0.1', @port)
259     sock.syswrite("GET / HTTP/1.0\r\nHost: foo\rbar\r\n\r\n")
260     assert_match %r{\AHTTP/1.[01] 400\b}, sock.sysread(4096)
261     assert_nil sock.close
262   end
264   def test_http_0_9
265     sock = TCPSocket.new('127.0.0.1', @port)
266     sock.syswrite("GET /hello\r\n")
267     assert_match 'hello!\n', sock.sysread(4096)
268     assert_nil sock.close
269   end
271   def test_header_is_too_long
272     redirect_test_io do
273       long = "GET /test HTTP/1.1\r\n" + ("X-Big: stuff\r\n" * 15000) + "\r\n"
274       assert_raises Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EINVAL, IOError do
275         do_test(long, long.length/2, 10)
276       end
277     end
278   end
280   def test_file_streamed_request
281     body = "a" * (Unicorn::Const::MAX_BODY * 2)
282     long = "PUT /test HTTP/1.1\r\nContent-length: #{body.length}\r\n\r\n" + body
283     do_test(long, Unicorn::Const::CHUNK_SIZE * 2 - 400)
284   end
286   def test_file_streamed_request_bad_body
287     body = "a" * (Unicorn::Const::MAX_BODY * 2)
288     long = "GET /test HTTP/1.1\r\nContent-ength: #{body.length}\r\n\r\n" + body
289     assert_raises(EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,
290                   Errno::EBADF) {
291       do_test(long, Unicorn::Const::CHUNK_SIZE * 2 - 400)
292     }
293   end
295   def test_listener_names
296     assert_equal [ "127.0.0.1:#@port" ], Unicorn.listener_names
297   end