unicorn 5.5.5 → 6.1.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.
@@ -12,6 +12,7 @@
12
12
  #include "common_field_optimization.h"
13
13
  #include "global_variables.h"
14
14
  #include "c_util.h"
15
+ #include "epollexclusive.h"
15
16
 
16
17
  void init_unicorn_httpdate(void);
17
18
 
@@ -65,18 +66,6 @@ struct http_parser {
65
66
  static ID id_set_backtrace, id_is_chunked_p;
66
67
  static VALUE cHttpParser;
67
68
 
68
- #ifdef HAVE_RB_HASH_CLEAR /* Ruby >= 2.0 */
69
- # define my_hash_clear(h) (void)rb_hash_clear(h)
70
- #else /* !HAVE_RB_HASH_CLEAR - Ruby <= 1.9.3 */
71
-
72
- static ID id_clear;
73
-
74
- static void my_hash_clear(VALUE h)
75
- {
76
- rb_funcall(h, id_clear, 0);
77
- }
78
- #endif /* HAVE_RB_HASH_CLEAR */
79
-
80
69
  static void finalize_header(struct http_parser *hp);
81
70
 
82
71
  static void parser_raise(VALUE klass, const char *msg)
@@ -650,7 +639,7 @@ static VALUE HttpParser_clear(VALUE self)
650
639
  return HttpParser_init(self);
651
640
 
652
641
  http_parser_init(hp);
653
- my_hash_clear(hp->env);
642
+ rb_hash_clear(hp->env);
654
643
 
655
644
  return self;
656
645
  }
@@ -979,6 +968,7 @@ void Init_unicorn_http(void)
979
968
  e414 = rb_define_class_under(mUnicorn, "RequestURITooLongError",
980
969
  eHttpParserError);
981
970
 
971
+ id_uminus = rb_intern("-@");
982
972
  init_globals();
983
973
  rb_define_alloc_func(cHttpParser, HttpParser_alloc);
984
974
  rb_define_method(cHttpParser, "initialize", HttpParser_init, 0);
@@ -1029,5 +1019,7 @@ void Init_unicorn_http(void)
1029
1019
  id_clear = rb_intern("clear");
1030
1020
  #endif
1031
1021
  id_is_chunked_p = rb_intern("is_chunked?");
1022
+
1023
+ init_epollexclusive(mUnicorn);
1032
1024
  }
1033
1025
  #undef SET_GLOBAL
@@ -53,6 +53,7 @@ class Unicorn::Configurator
53
53
  server.logger.info("worker=#{worker.nr} ready")
54
54
  },
55
55
  :pid => nil,
56
+ :early_hints => false,
56
57
  :worker_exec => false,
57
58
  :preload_app => false,
58
59
  :check_client_connection => false,
@@ -276,6 +277,15 @@ class Unicorn::Configurator
276
277
  set_bool(:default_middleware, bool)
277
278
  end
278
279
 
280
+ # sets whether to enable the proposed early hints Rack API.
281
+ # If enabled, Rails 5.2+ will automatically send a 103 Early Hint
282
+ # for all the `javascript_include_tag` and `stylesheet_link_tag`
283
+ # in your response. See: https://api.rubyonrails.org/v5.2/classes/ActionDispatch/Request.html#method-i-send_early_hints
284
+ # See also https://tools.ietf.org/html/rfc8297
285
+ def early_hints(bool)
286
+ set_bool(:early_hints, bool)
287
+ end
288
+
279
289
  # sets listeners to the given +addresses+, replacing or augmenting the
280
290
  # current set. This is for the global listener pool shared by all
281
291
  # worker processes. For per-worker listeners, see the after_fork example
@@ -62,7 +62,6 @@ class Unicorn::HttpParser
62
62
  # This does minimal exception trapping and it is up to the caller
63
63
  # to handle any socket errors (e.g. user aborted upload).
64
64
  def read(socket)
65
- clear
66
65
  e = env
67
66
 
68
67
  # From https://www.ietf.org/rfc/rfc3875:
@@ -15,7 +15,7 @@ class Unicorn::HttpServer
15
15
  :before_fork, :after_fork, :before_exec,
16
16
  :listener_opts, :preload_app,
17
17
  :orig_app, :config, :ready_pipe, :user,
18
- :default_middleware
18
+ :default_middleware, :early_hints
19
19
  attr_writer :after_worker_exit, :after_worker_ready, :worker_exec
20
20
 
21
21
  attr_reader :pid, :logger
@@ -69,7 +69,6 @@ class Unicorn::HttpServer
69
69
  # incoming requests on the socket.
70
70
  def initialize(app, options = {})
71
71
  @app = app
72
- @request = Unicorn::HttpRequest.new
73
72
  @reexec_pid = 0
74
73
  @default_middleware = true
75
74
  options = options.dup
@@ -588,10 +587,29 @@ class Unicorn::HttpServer
588
587
  rescue
589
588
  end
590
589
 
590
+ def e103_response_write(client, headers)
591
+ response = if @request.response_start_sent
592
+ "103 Early Hints\r\n"
593
+ else
594
+ "HTTP/1.1 103 Early Hints\r\n"
595
+ end
596
+
597
+ headers.each_pair do |k, vs|
598
+ next if !vs || vs.empty?
599
+ values = vs.to_s.split("\n".freeze)
600
+ values.each do |v|
601
+ response << "#{k}: #{v}\r\n"
602
+ end
603
+ end
604
+ response << "\r\n".freeze
605
+ response << "HTTP/1.1 ".freeze if @request.response_start_sent
606
+ client.write(response)
607
+ end
608
+
591
609
  def e100_response_write(client, env)
592
610
  # We use String#freeze to avoid allocations under Ruby 2.1+
593
611
  # Not many users hit this code path, so it's better to reduce the
594
- # constant table sizes even for 1.9.3-2.0 users who'll hit extra
612
+ # constant table sizes even for Ruby 2.0 users who'll hit extra
595
613
  # allocations here.
596
614
  client.write(@request.response_start_sent ?
597
615
  "100 Continue\r\n\r\nHTTP/1.1 ".freeze :
@@ -602,7 +620,18 @@ class Unicorn::HttpServer
602
620
  # once a client is accepted, it is processed in its entirety here
603
621
  # in 3 easy steps: read request, call app, write app response
604
622
  def process_client(client)
605
- status, headers, body = @app.call(env = @request.read(client))
623
+ @request = Unicorn::HttpRequest.new
624
+ env = @request.read(client)
625
+
626
+ if early_hints
627
+ env["rack.early_hints"] = lambda do |headers|
628
+ e103_response_write(client, headers)
629
+ end
630
+ end
631
+
632
+ env["rack.after_reply"] = []
633
+
634
+ status, headers, body = @app.call(env)
606
635
 
607
636
  begin
608
637
  return if @request.hijacked?
@@ -624,6 +653,8 @@ class Unicorn::HttpServer
624
653
  end
625
654
  rescue => e
626
655
  handle_error(client, e)
656
+ ensure
657
+ env["rack.after_reply"].each(&:call) if env
627
658
  end
628
659
 
629
660
  def nuke_listeners!(readers)
@@ -654,7 +685,6 @@ class Unicorn::HttpServer
654
685
  LISTENERS.each { |sock| sock.close_on_exec = true }
655
686
 
656
687
  worker.user(*user) if user.kind_of?(Array) && ! worker.switched
657
- self.timeout /= 2.0 # halve it for select()
658
688
  @config = nil
659
689
  build_app! unless preload_app
660
690
  @after_fork = @listener_opts = @orig_app = nil
@@ -668,59 +698,55 @@ class Unicorn::HttpServer
668
698
  logger.info "worker=#{worker_nr} reopening logs..."
669
699
  Unicorn::Util.reopen_logs
670
700
  logger.info "worker=#{worker_nr} done reopening logs"
701
+ false
671
702
  rescue => e
672
703
  logger.error(e) rescue nil
673
704
  exit!(77) # EX_NOPERM in sysexits.h
674
705
  end
675
706
 
707
+ def prep_readers(readers)
708
+ wtr = Unicorn::Waiter.prep_readers(readers)
709
+ @timeout *= 500 # to milliseconds for epoll, but halved
710
+ wtr
711
+ rescue
712
+ require_relative 'select_waiter'
713
+ @timeout /= 2.0 # halved for IO.select
714
+ Unicorn::SelectWaiter.new
715
+ end
716
+
676
717
  # runs inside each forked worker, this sits around and waits
677
718
  # for connections and doesn't die until the parent dies (or is
678
719
  # given a INT, QUIT, or TERM signal)
679
720
  def worker_loop(worker)
680
- ppid = @master_pid
681
721
  readers = init_worker_process(worker)
682
- nr = 0 # this becomes negative if we need to reopen logs
722
+ waiter = prep_readers(readers)
723
+ reopen = false
683
724
 
684
725
  # this only works immediately if the master sent us the signal
685
726
  # (which is the normal case)
686
- trap(:USR1) { nr = -65536 }
727
+ trap(:USR1) { reopen = true }
687
728
 
688
729
  ready = readers.dup
689
- nr_listeners = readers.size
690
730
  @after_worker_ready.call(self, worker)
691
731
 
692
732
  begin
693
- nr < 0 and reopen_worker_logs(worker.nr)
694
- nr = 0
733
+ reopen = reopen_worker_logs(worker.nr) if reopen
695
734
  worker.tick = time_now.to_i
696
- tmp = ready.dup
697
- while sock = tmp.shift
735
+ while sock = ready.shift
698
736
  # Unicorn::Worker#kgio_tryaccept is not like accept(2) at all,
699
737
  # but that will return false
700
738
  if client = sock.kgio_tryaccept
701
739
  process_client(client)
702
- nr += 1
703
740
  worker.tick = time_now.to_i
704
741
  end
705
- break if nr < 0
742
+ break if reopen
706
743
  end
707
744
 
708
- # make the following bet: if we accepted clients this round,
709
- # we're probably reasonably busy, so avoid calling select()
710
- # and do a speculative non-blocking accept() on ready listeners
711
- # before we sleep again in select().
712
- if nr == nr_listeners
713
- tmp = ready.dup
714
- redo
715
- end
716
-
717
- ppid == Process.ppid or return
718
-
719
- # timeout used so we can detect parent death:
745
+ # timeout so we can .tick and keep parent from SIGKILL-ing us
720
746
  worker.tick = time_now.to_i
721
- ret = IO.select(readers, nil, nil, @timeout) and ready = ret[0]
747
+ waiter.get_readers(ready, readers, @timeout)
722
748
  rescue => e
723
- redo if nr < 0 && readers[0]
749
+ redo if reopen && readers[0]
724
750
  Unicorn.log_error(@logger, "listen loop error", e) if readers[0]
725
751
  end while readers[0]
726
752
  end
@@ -60,7 +60,6 @@ module Unicorn::OobGC
60
60
  self.const_set :OOBGC_INTERVAL, interval
61
61
  ObjectSpace.each_object(Unicorn::HttpServer) do |s|
62
62
  s.extend(self)
63
- self.const_set :OOBGC_ENV, s.instance_variable_get(:@request).env
64
63
  end
65
64
  app # pretend to be Rack middleware since it was in the past
66
65
  end
@@ -68,9 +67,10 @@ module Unicorn::OobGC
68
67
  #:stopdoc:
69
68
  def process_client(client)
70
69
  super(client) # Unicorn::HttpServer#process_client
71
- if OOBGC_PATH =~ OOBGC_ENV['PATH_INFO'] && ((@@nr -= 1) <= 0)
70
+ env = instance_variable_get(:@request).env
71
+ if OOBGC_PATH =~ env['PATH_INFO'] && ((@@nr -= 1) <= 0)
72
72
  @@nr = OOBGC_INTERVAL
73
- OOBGC_ENV.clear
73
+ env.clear
74
74
  disabled = GC.enable
75
75
  GC.start
76
76
  GC.disable if disabled
@@ -0,0 +1,6 @@
1
+ # fallback for non-Linux and Linux <4.5 systems w/o EPOLLEXCLUSIVE
2
+ class Unicorn::SelectWaiter # :nodoc:
3
+ def get_readers(ready, readers, timeout) # :nodoc:
4
+ ret = IO.select(readers, nil, nil, timeout) and ready.replace(ret[0])
5
+ end
6
+ end
@@ -1 +1 @@
1
- Unicorn::Const::UNICORN_VERSION = '5.5.5'
1
+ Unicorn::Const::UNICORN_VERSION = '6.1.0'
data/lib/unicorn.rb CHANGED
@@ -114,8 +114,6 @@ module Unicorn
114
114
 
115
115
  def self.pipe # :nodoc:
116
116
  Kgio::Pipe.new.each do |io|
117
- io.close_on_exec = true # remove this when we only support Ruby >= 2.0
118
-
119
117
  # shrink pipes to minimize impact on /proc/sys/fs/pipe-user-pages-soft
120
118
  # limits.
121
119
  if defined?(F_SETPIPE_SZ)
data/t/GNUmakefile CHANGED
@@ -1,74 +1,5 @@
1
- # we can run tests in parallel with GNU make
1
+ # there used to be more, here, but we stopped relying on recursive make
2
2
  all::
3
+ $(MAKE) -C .. test-integration
3
4
 
4
- pid := $(shell echo $$PPID)
5
-
6
- RUBY = ruby
7
- RAKE = rake
8
- -include ../local.mk
9
- ifeq ($(RUBY_VERSION),)
10
- RUBY_VERSION := $(shell $(RUBY) -e 'puts RUBY_VERSION')
11
- endif
12
-
13
- ifeq ($(RUBY_VERSION),)
14
- $(error unable to detect RUBY_VERSION)
15
- endif
16
-
17
- RUBY_ENGINE := $(shell $(RUBY) -e 'puts((RUBY_ENGINE rescue "ruby"))')
18
- export RUBY_ENGINE
19
-
20
- MYLIBS := $(RUBYLIB)
21
-
22
- T = $(wildcard t[0-9][0-9][0-9][0-9]-*.sh)
23
-
24
- all:: $(T)
25
-
26
- # can't rely on "set -o pipefail" since we don't require bash or ksh93 :<
27
- t_pfx = trash/$@-$(RUBY_ENGINE)-$(RUBY_VERSION)
28
- TEST_OPTS =
29
- # TRACER = strace -f -o $(t_pfx).strace -s 100000
30
- # TRACER = /usr/bin/time -o $(t_pfx).time
31
-
32
- ifdef V
33
- ifeq ($(V),2)
34
- TEST_OPTS += --trace
35
- else
36
- TEST_OPTS += --verbose
37
- endif
38
- endif
39
-
40
- random_blob:
41
- dd if=/dev/urandom bs=1M count=30 of=$@.$(pid)
42
- mv $@.$(pid) $@
43
-
44
- $(T): random_blob
45
-
46
- dependencies := socat curl
47
- deps := $(addprefix .dep+,$(dependencies))
48
- $(deps): dep_bin = $(lastword $(subst +, ,$@))
49
- $(deps):
50
- @which $(dep_bin) > $@.$(pid) 2>/dev/null || :
51
- @test -s $@.$(pid) || \
52
- { echo >&2 "E '$(dep_bin)' not found in PATH=$(PATH)"; exit 1; }
53
- @mv $@.$(pid) $@
54
- dep: $(deps)
55
-
56
- test_prefix := $(CURDIR)/../test/$(RUBY_ENGINE)-$(RUBY_VERSION)
57
- $(test_prefix)/.stamp:
58
- $(MAKE) -C .. test-install
59
-
60
- $(T): export RUBY := $(RUBY)
61
- $(T): export RAKE := $(RAKE)
62
- $(T): export PATH := $(test_prefix)/bin:$(PATH)
63
- $(T): export RUBYLIB := $(test_prefix)/lib:$(MYLIBS)
64
- $(T): dep $(test_prefix)/.stamp trash/.gitignore
65
- $(TRACER) $(SHELL) $(SH_TEST_OPTS) $@ $(TEST_OPTS)
66
-
67
- trash/.gitignore:
68
- mkdir -p $(@D)
69
- echo '*' > $@
70
-
71
- clean:
72
- $(RM) -r trash/*
73
-
74
- .PHONY: $(T) clean
5
+ .PHONY: all
data/t/README CHANGED
@@ -10,7 +10,7 @@ comfortable writing integration tests with.
10
10
 
11
11
  == Requirements
12
12
 
13
- * {Ruby 1.9.3+}[https://www.ruby-lang.org/en/] (duh!)
13
+ * {Ruby 2.0.0+}[https://www.ruby-lang.org/en/] (duh!)
14
14
  * {GNU make}[https://www.gnu.org/software/make/]
15
15
  * {socat}[http://www.dest-unreach.org/socat/]
16
16
  * {curl}[https://curl.haxx.se/]
data/t/test-lib.sh CHANGED
@@ -94,7 +94,8 @@ check_stderr () {
94
94
  set +u
95
95
  _r_err=${1-${r_err}}
96
96
  set -u
97
- if grep -v $T $_r_err | grep -i Error
97
+ if grep -v $T $_r_err | grep -i Error | \
98
+ grep -v NameError.*Unicorn::Waiter
98
99
  then
99
100
  die "Errors found in $_r_err"
100
101
  elif grep SIGKILL $_r_err
@@ -574,7 +574,7 @@ EOF
574
574
  assert_equal String, results[0].class
575
575
  worker_pid = results[0].to_i
576
576
  assert_not_equal pid, worker_pid
577
- s = UNIXSocket.new(tmp.path)
577
+ s = unix_socket(tmp.path)
578
578
  s.syswrite("GET / HTTP/1.0\r\n\r\n")
579
579
  results = ''
580
580
  loop { results << s.sysread(4096) } rescue nil
@@ -732,7 +732,7 @@ EOF
732
732
  wait_for_file(sock_path)
733
733
  assert File.socket?(sock_path)
734
734
 
735
- sock = UNIXSocket.new(sock_path)
735
+ sock = unix_socket(sock_path)
736
736
  sock.syswrite("GET / HTTP/1.0\r\n\r\n")
737
737
  results = sock.sysread(4096)
738
738
 
@@ -742,7 +742,7 @@ EOF
742
742
  wait_for_file(sock_path)
743
743
  assert File.socket?(sock_path)
744
744
 
745
- sock = UNIXSocket.new(sock_path)
745
+ sock = unix_socket(sock_path)
746
746
  sock.syswrite("GET / HTTP/1.0\r\n\r\n")
747
747
  results = sock.sysread(4096)
748
748
 
@@ -777,7 +777,7 @@ EOF
777
777
  assert_equal pid, File.read(pid_file).to_i
778
778
  assert File.socket?(sock_path), "socket created"
779
779
 
780
- sock = UNIXSocket.new(sock_path)
780
+ sock = unix_socket(sock_path)
781
781
  sock.syswrite("GET / HTTP/1.0\r\n\r\n")
782
782
  results = sock.sysread(4096)
783
783
 
@@ -803,7 +803,7 @@ EOF
803
803
  wait_for_file(new_sock_path)
804
804
  assert File.socket?(new_sock_path), "socket exists"
805
805
  @sockets.each do |path|
806
- sock = UNIXSocket.new(path)
806
+ sock = unix_socket(path)
807
807
  sock.syswrite("GET / HTTP/1.0\r\n\r\n")
808
808
  results = sock.sysread(4096)
809
809
  assert_equal String, results.class
data/test/test_helper.rb CHANGED
@@ -28,22 +28,43 @@ require 'tempfile'
28
28
  require 'fileutils'
29
29
  require 'logger'
30
30
  require 'unicorn'
31
+ require 'io/nonblock'
31
32
 
32
33
  if ENV['DEBUG']
33
34
  require 'ruby-debug'
34
35
  Debugger.start
35
36
  end
36
37
 
38
+ unless RUBY_VERSION < '3.1'
39
+ warn "Unicorn was only tested against MRI up to 3.0.\n" \
40
+ "It might not properly work with #{RUBY_VERSION}"
41
+ end
42
+
37
43
  def redirect_test_io
38
44
  orig_err = STDERR.dup
39
45
  orig_out = STDOUT.dup
40
- STDERR.reopen("test_stderr.#{$$}.log", "a")
41
- STDOUT.reopen("test_stdout.#{$$}.log", "a")
46
+ rdr_pid = $$
47
+ new_out = File.open("test_stdout.#$$.log", "a")
48
+ new_err = File.open("test_stderr.#$$.log", "a")
49
+ new_out.sync = new_err.sync = true
50
+
51
+ if tail = ENV['TAIL'] # "tail -F" if GNU, "tail -f" otherwise
52
+ require 'shellwords'
53
+ cmd = tail.shellsplit
54
+ cmd << new_out.path
55
+ cmd << new_err.path
56
+ pid = Process.spawn(*cmd, { 1 => 2, :pgroup => true })
57
+ sleep 0.1 # wait for tail(1) to startup
58
+ end
59
+ STDERR.reopen(new_err)
60
+ STDOUT.reopen(new_out)
42
61
  STDERR.sync = STDOUT.sync = true
43
62
 
44
63
  at_exit do
45
- File.unlink("test_stderr.#{$$}.log") rescue nil
46
- File.unlink("test_stdout.#{$$}.log") rescue nil
64
+ if rdr_pid == $$
65
+ File.unlink(new_out.path) rescue nil
66
+ File.unlink(new_err.path) rescue nil
67
+ end
47
68
  end
48
69
 
49
70
  begin
@@ -51,6 +72,7 @@ def redirect_test_io
51
72
  ensure
52
73
  STDERR.reopen(orig_err)
53
74
  STDOUT.reopen(orig_out)
75
+ Process.kill(:TERM, pid) if pid
54
76
  end
55
77
  end
56
78
 
@@ -270,3 +292,15 @@ def reset_sig_handlers
270
292
  trap(sig, "DEFAULT")
271
293
  end
272
294
  end
295
+
296
+ def tcp_socket(*args)
297
+ sock = TCPSocket.new(*args)
298
+ sock.nonblock = false
299
+ sock
300
+ end
301
+
302
+ def unix_socket(*args)
303
+ sock = UNIXSocket.new(*args)
304
+ sock.nonblock = false
305
+ sock
306
+ end
@@ -3,6 +3,7 @@ require 'unicorn'
3
3
  require 'io/wait'
4
4
  require 'tempfile'
5
5
  require 'test/unit'
6
+ require './test/test_helper'
6
7
 
7
8
  class TestCccTCPI < Test::Unit::TestCase
8
9
  def test_ccc_tcpi
@@ -42,7 +43,7 @@ class TestCccTCPI < Test::Unit::TestCase
42
43
  wr.close
43
44
 
44
45
  # make sure the server is running, at least
45
- client = TCPSocket.new(host, port)
46
+ client = tcp_socket(host, port)
46
47
  client.write("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
47
48
  assert client.wait(10), 'never got response from server'
48
49
  res = client.read
@@ -51,13 +52,13 @@ class TestCccTCPI < Test::Unit::TestCase
51
52
  client.close
52
53
 
53
54
  # start a slow request...
54
- sleeper = TCPSocket.new(host, port)
55
+ sleeper = tcp_socket(host, port)
55
56
  sleeper.write("GET /sleep HTTP/1.1\r\nHost: example.com\r\n\r\n")
56
57
 
57
58
  # and a bunch of aborted ones
58
59
  nr = 100
59
60
  nr.times do |i|
60
- client = TCPSocket.new(host, port)
61
+ client = tcp_socket(host, port)
61
62
  client.write("GET /collections/#{rand(10000)} HTTP/1.1\r\n" \
62
63
  "Host: example.com\r\n\r\n")
63
64
  client.close
@@ -23,6 +23,34 @@ class TestHandler
23
23
  end
24
24
  end
25
25
 
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
36
+
37
+ class TestRackAfterReply
38
+ def initialize
39
+ @called = false
40
+ end
41
+
42
+ def call(env)
43
+ while env['rack.input'].read(4096)
44
+ end
45
+
46
+ env["rack.after_reply"] << -> { @called = true }
47
+
48
+ [200, { 'Content-Type' => 'text/plain' }, ["after_reply_called: #{@called}"]]
49
+ rescue Unicorn::ClientShutdown, Unicorn::HttpParserError => e
50
+ $stderr.syswrite("#{e.class}: #{e.message} #{e.backtrace.empty?}\n")
51
+ raise e
52
+ end
53
+ end
26
54
 
27
55
  class WebServerTest < Test::Unit::TestCase
28
56
 
@@ -84,6 +112,52 @@ class WebServerTest < Test::Unit::TestCase
84
112
  tmp.close!
85
113
  end
86
114
 
115
+ def test_early_hints
116
+ teardown
117
+ redirect_test_io do
118
+ @server = HttpServer.new(TestEarlyHintsHandler.new,
119
+ :listeners => [ "127.0.0.1:#@port"],
120
+ :early_hints => true)
121
+ @server.start
122
+ end
123
+
124
+ sock = tcp_socket('127.0.0.1', @port)
125
+ sock.syswrite("GET / HTTP/1.0\r\n\r\n")
126
+
127
+ responses = sock.read(4096)
128
+ assert_match %r{\AHTTP/1.[01] 103\b}, responses
129
+ assert_match %r{^Link: </style\.css>}, responses
130
+ assert_match %r{^Link: </script\.js>}, responses
131
+
132
+ assert_match %r{^HTTP/1.[01] 200\b}, responses
133
+ end
134
+
135
+ def test_after_reply
136
+ teardown
137
+
138
+ redirect_test_io do
139
+ @server = HttpServer.new(TestRackAfterReply.new,
140
+ :listeners => [ "127.0.0.1:#@port"])
141
+ @server.start
142
+ end
143
+
144
+ sock = tcp_socket('127.0.0.1', @port)
145
+ sock.syswrite("GET / HTTP/1.0\r\n\r\n")
146
+
147
+ responses = sock.read(4096)
148
+ assert_match %r{\AHTTP/1.[01] 200\b}, responses
149
+ assert_match %r{^after_reply_called: false}, responses
150
+
151
+ sock = tcp_socket('127.0.0.1', @port)
152
+ sock.syswrite("GET / HTTP/1.0\r\n\r\n")
153
+
154
+ responses = sock.read(4096)
155
+ assert_match %r{\AHTTP/1.[01] 200\b}, responses
156
+ assert_match %r{^after_reply_called: true}, responses
157
+
158
+ sock.close
159
+ end
160
+
87
161
  def test_broken_app
88
162
  teardown
89
163
  app = lambda { |env| raise RuntimeError, "hello" }
@@ -92,7 +166,7 @@ class WebServerTest < Test::Unit::TestCase
92
166
  @server = HttpServer.new(app, :listeners => [ "127.0.0.1:#@port"] )
93
167
  @server.start
94
168
  end
95
- sock = TCPSocket.new('127.0.0.1', @port)
169
+ sock = tcp_socket('127.0.0.1', @port)
96
170
  sock.syswrite("GET / HTTP/1.0\r\n\r\n")
97
171
  assert_match %r{\AHTTP/1.[01] 500\b}, sock.sysread(4096)
98
172
  assert_nil sock.close
@@ -105,7 +179,7 @@ class WebServerTest < Test::Unit::TestCase
105
179
 
106
180
  def test_client_shutdown_writes
107
181
  bs = 15609315 * rand
108
- sock = TCPSocket.new('127.0.0.1', @port)
182
+ sock = tcp_socket('127.0.0.1', @port)
109
183
  sock.syswrite("PUT /hello HTTP/1.1\r\n")
110
184
  sock.syswrite("Host: example.com\r\n")
111
185
  sock.syswrite("Transfer-Encoding: chunked\r\n")
@@ -132,7 +206,7 @@ class WebServerTest < Test::Unit::TestCase
132
206
 
133
207
  def test_client_shutdown_write_truncates
134
208
  bs = 15609315 * rand
135
- sock = TCPSocket.new('127.0.0.1', @port)
209
+ sock = tcp_socket('127.0.0.1', @port)
136
210
  sock.syswrite("PUT /hello HTTP/1.1\r\n")
137
211
  sock.syswrite("Host: example.com\r\n")
138
212
  sock.syswrite("Transfer-Encoding: chunked\r\n")
@@ -158,7 +232,7 @@ class WebServerTest < Test::Unit::TestCase
158
232
 
159
233
  def test_client_malformed_body
160
234
  bs = 15653984
161
- sock = TCPSocket.new('127.0.0.1', @port)
235
+ sock = tcp_socket('127.0.0.1', @port)
162
236
  sock.syswrite("PUT /hello HTTP/1.1\r\n")
163
237
  sock.syswrite("Host: example.com\r\n")
164
238
  sock.syswrite("Transfer-Encoding: chunked\r\n")
@@ -180,7 +254,7 @@ class WebServerTest < Test::Unit::TestCase
180
254
 
181
255
  def do_test(string, chunk, close_after=nil, shutdown_delay=0)
182
256
  # Do not use instance variables here, because it needs to be thread safe
183
- socket = TCPSocket.new("127.0.0.1", @port);
257
+ socket = tcp_socket("127.0.0.1", @port);
184
258
  request = StringIO.new(string)
185
259
  chunks_out = 0
186
260
 
@@ -225,14 +299,14 @@ class WebServerTest < Test::Unit::TestCase
225
299
  end
226
300
 
227
301
  def test_bad_client_400
228
- sock = TCPSocket.new('127.0.0.1', @port)
302
+ sock = tcp_socket('127.0.0.1', @port)
229
303
  sock.syswrite("GET / HTTP/1.0\r\nHost: foo\rbar\r\n\r\n")
230
304
  assert_match %r{\AHTTP/1.[01] 400\b}, sock.sysread(4096)
231
305
  assert_nil sock.close
232
306
  end
233
307
 
234
308
  def test_http_0_9
235
- sock = TCPSocket.new('127.0.0.1', @port)
309
+ sock = tcp_socket('127.0.0.1', @port)
236
310
  sock.syswrite("GET /hello\r\n")
237
311
  assert_match 'hello!\n', sock.sysread(4096)
238
312
  assert_nil sock.close