about summary refs log tree commit homepage
path: root/test/unit
diff options
context:
space:
mode:
Diffstat (limited to 'test/unit')
-rw-r--r--test/unit/test_configurator.rb65
-rw-r--r--test/unit/test_http_parser.rb304
-rw-r--r--test/unit/test_http_parser_ng.rb420
-rw-r--r--test/unit/test_request.rb43
-rw-r--r--test/unit/test_response.rb13
-rw-r--r--test/unit/test_server.rb132
-rw-r--r--test/unit/test_signals.rb31
-rw-r--r--test/unit/test_socket_helper.rb16
-rw-r--r--test/unit/test_tee_input.rb229
-rw-r--r--test/unit/test_upload.rb236
-rw-r--r--test/unit/test_util.rb7
11 files changed, 1313 insertions, 183 deletions
diff --git a/test/unit/test_configurator.rb b/test/unit/test_configurator.rb
index 98f2db6..ac1efa8 100644
--- a/test/unit/test_configurator.rb
+++ b/test/unit/test_configurator.rb
@@ -1,7 +1,11 @@
+# -*- encoding: binary -*-
+
 require 'test/unit'
 require 'tempfile'
-require 'unicorn/configurator'
+require 'unicorn'
 
+TestStruct = Struct.new(
+  *(Unicorn::Configurator::DEFAULTS.keys + %w(listener_opts listeners)))
 class TestConfigurator < Test::Unit::TestCase
 
   def test_config_init
@@ -28,8 +32,10 @@ class TestConfigurator < Test::Unit::TestCase
     assert_equal "0.0.0.0:2007", meth.call('*:2007')
     assert_equal "0.0.0.0:2007", meth.call('2007')
     assert_equal "0.0.0.0:2007", meth.call(2007)
-    assert_match %r{\A\d+\.\d+\.\d+\.\d+:2007\z}, meth.call('1:2007')
-    assert_match %r{\A\d+\.\d+\.\d+\.\d+:2007\z}, meth.call('2:2007')
+
+    # the next two aren't portable, consider them unsupported for now
+    # assert_match %r{\A\d+\.\d+\.\d+\.\d+:2007\z}, meth.call('1:2007')
+    # assert_match %r{\A\d+\.\d+\.\d+\.\d+:2007\z}, meth.call('2:2007')
   end
 
   def test_config_invalid
@@ -51,22 +57,23 @@ class TestConfigurator < Test::Unit::TestCase
 
   def test_config_defaults
     cfg = Unicorn::Configurator.new(:use_defaults => true)
-    assert_nothing_raised { cfg.commit!(self) }
+    test_struct = TestStruct.new
+    assert_nothing_raised { cfg.commit!(test_struct) }
     Unicorn::Configurator::DEFAULTS.each do |key,value|
-      assert_equal value, instance_variable_get("@#{key.to_s}")
+      assert_equal value, test_struct.__send__(key)
     end
   end
 
   def test_config_defaults_skip
     cfg = Unicorn::Configurator.new(:use_defaults => true)
     skip = [ :logger ]
-    assert_nothing_raised { cfg.commit!(self, :skip => skip) }
-    @logger = nil
+    test_struct = TestStruct.new
+    assert_nothing_raised { cfg.commit!(test_struct, :skip => skip) }
     Unicorn::Configurator::DEFAULTS.each do |key,value|
       next if skip.include?(key)
-      assert_equal value, instance_variable_get("@#{key.to_s}")
+      assert_equal value, test_struct.__send__(key)
     end
-    assert_nil @logger
+    assert_nil test_struct.logger
   end
 
   def test_listen_options
@@ -78,8 +85,9 @@ class TestConfigurator < Test::Unit::TestCase
     assert_nothing_raised do
       cfg = Unicorn::Configurator.new(:config_file => tmp.path)
     end
-    assert_nothing_raised { cfg.commit!(self) }
-    assert(listener_opts = instance_variable_get("@listener_opts"))
+    test_struct = TestStruct.new
+    assert_nothing_raised { cfg.commit!(test_struct) }
+    assert(listener_opts = test_struct.listener_opts)
     assert_equal expect, listener_opts[listener]
   end
 
@@ -93,10 +101,41 @@ class TestConfigurator < Test::Unit::TestCase
     end
   end
 
+  def test_listen_option_bad_delay
+    tmp = Tempfile.new('unicorn_config')
+    expect = { :delay => "five" }
+    listener = "127.0.0.1:12345"
+    tmp.syswrite("listen '#{listener}', #{expect.inspect}\n")
+    assert_raises(ArgumentError) do
+      Unicorn::Configurator.new(:config_file => tmp.path)
+    end
+  end
+
+  def test_listen_option_float_delay
+    tmp = Tempfile.new('unicorn_config')
+    expect = { :delay => 0.5 }
+    listener = "127.0.0.1:12345"
+    tmp.syswrite("listen '#{listener}', #{expect.inspect}\n")
+    assert_nothing_raised do
+      Unicorn::Configurator.new(:config_file => tmp.path)
+    end
+  end
+
+  def test_listen_option_int_delay
+    tmp = Tempfile.new('unicorn_config')
+    expect = { :delay => 5 }
+    listener = "127.0.0.1:12345"
+    tmp.syswrite("listen '#{listener}', #{expect.inspect}\n")
+    assert_nothing_raised do
+      Unicorn::Configurator.new(:config_file => tmp.path)
+    end
+  end
+
   def test_after_fork_proc
+    test_struct = TestStruct.new
     [ proc { |a,b| }, Proc.new { |a,b| }, lambda { |a,b| } ].each do |my_proc|
-      Unicorn::Configurator.new(:after_fork => my_proc).commit!(self)
-      assert_equal my_proc, @after_fork
+      Unicorn::Configurator.new(:after_fork => my_proc).commit!(test_struct)
+      assert_equal my_proc, test_struct.after_fork
     end
   end
 
diff --git a/test/unit/test_http_parser.rb b/test/unit/test_http_parser.rb
index a158ebb..0443b46 100644
--- a/test/unit/test_http_parser.rb
+++ b/test/unit/test_http_parser.rb
@@ -1,3 +1,5 @@
+# -*- encoding: binary -*-
+
 # Copyright (c) 2005 Zed A. Shaw
 # You can redistribute it and/or modify it under the same terms as Ruby.
 #
@@ -9,12 +11,13 @@ require 'test/test_helper'
 include Unicorn
 
 class HttpParserTest < Test::Unit::TestCase
-    
+
   def test_parse_simple
     parser = HttpParser.new
     req = {}
     http = "GET / HTTP/1.1\r\n\r\n"
-    assert parser.execute(req, http)
+    assert_equal req, parser.headers(req, http)
+    assert_equal '', http
 
     assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL']
     assert_equal '/', req['REQUEST_PATH']
@@ -24,15 +27,18 @@ class HttpParserTest < Test::Unit::TestCase
     assert_nil req['FRAGMENT']
     assert_equal '', req['QUERY_STRING']
 
+    assert parser.keepalive?
     parser.reset
     req.clear
 
-    assert ! parser.execute(req, "G")
+    http = "G"
+    assert_nil parser.headers(req, http)
+    assert_equal "G", http
     assert req.empty?
 
     # try parsing again to ensure we were reset correctly
     http = "GET /hello-world HTTP/1.1\r\n\r\n"
-    assert parser.execute(req, http)
+    assert parser.headers(req, http)
 
     assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL']
     assert_equal '/hello-world', req['REQUEST_PATH']
@@ -41,55 +47,184 @@ class HttpParserTest < Test::Unit::TestCase
     assert_equal 'GET', req['REQUEST_METHOD']
     assert_nil req['FRAGMENT']
     assert_equal '', req['QUERY_STRING']
+    assert_equal '', http
+    assert parser.keepalive?
+  end
+
+  def test_connection_close_no_ka
+    parser = HttpParser.new
+    req = {}
+    tmp = "GET / HTTP/1.1\r\nConnection: close\r\n\r\n"
+    assert_equal req.object_id, parser.headers(req, tmp).object_id
+    assert_equal "GET", req['REQUEST_METHOD']
+    assert ! parser.keepalive?
+  end
+
+  def test_connection_keep_alive_ka
+    parser = HttpParser.new
+    req = {}
+    tmp = "HEAD / HTTP/1.1\r\nConnection: keep-alive\r\n\r\n"
+    assert_equal req.object_id, parser.headers(req, tmp).object_id
+    assert parser.keepalive?
+  end
+
+  def test_connection_keep_alive_ka_bad_method
+    parser = HttpParser.new
+    req = {}
+    tmp = "POST / HTTP/1.1\r\nConnection: keep-alive\r\n\r\n"
+    assert_equal req.object_id, parser.headers(req, tmp).object_id
+    assert ! parser.keepalive?
+  end
+
+  def test_connection_keep_alive_ka_bad_version
+    parser = HttpParser.new
+    req = {}
+    tmp = "GET / HTTP/1.0\r\nConnection: keep-alive\r\n\r\n"
+    assert_equal req.object_id, parser.headers(req, tmp).object_id
+    assert parser.keepalive?
   end
 
   def test_parse_server_host_default_port
     parser = HttpParser.new
     req = {}
-    assert parser.execute(req, "GET / HTTP/1.1\r\nHost: foo\r\n\r\n")
+    tmp = "GET / HTTP/1.1\r\nHost: foo\r\n\r\n"
+    assert_equal req, parser.headers(req, tmp)
     assert_equal 'foo', req['SERVER_NAME']
     assert_equal '80', req['SERVER_PORT']
+    assert_equal '', tmp
+    assert parser.keepalive?
   end
 
   def test_parse_server_host_alt_port
     parser = HttpParser.new
     req = {}
-    assert parser.execute(req, "GET / HTTP/1.1\r\nHost: foo:999\r\n\r\n")
+    tmp = "GET / HTTP/1.1\r\nHost: foo:999\r\n\r\n"
+    assert_equal req, parser.headers(req, tmp)
     assert_equal 'foo', req['SERVER_NAME']
     assert_equal '999', req['SERVER_PORT']
+    assert_equal '', tmp
+    assert parser.keepalive?
   end
 
   def test_parse_server_host_empty_port
     parser = HttpParser.new
     req = {}
-    assert parser.execute(req, "GET / HTTP/1.1\r\nHost: foo:\r\n\r\n")
+    tmp = "GET / HTTP/1.1\r\nHost: foo:\r\n\r\n"
+    assert_equal req, parser.headers(req, tmp)
     assert_equal 'foo', req['SERVER_NAME']
     assert_equal '80', req['SERVER_PORT']
+    assert_equal '', tmp
+    assert parser.keepalive?
   end
 
   def test_parse_server_host_xfp_https
     parser = HttpParser.new
     req = {}
-    assert parser.execute(req, "GET / HTTP/1.1\r\nHost: foo:\r\n" \
-                          "X-Forwarded-Proto: https\r\n\r\n")
+    tmp = "GET / HTTP/1.1\r\nHost: foo:\r\n" \
+          "X-Forwarded-Proto: https\r\n\r\n"
+    assert_equal req, parser.headers(req, tmp)
     assert_equal 'foo', req['SERVER_NAME']
     assert_equal '443', req['SERVER_PORT']
+    assert_equal '', tmp
+    assert parser.keepalive?
   end
 
   def test_parse_strange_headers
     parser = HttpParser.new
     req = {}
     should_be_good = "GET / HTTP/1.1\r\naaaaaaaaaaaaa:++++++++++\r\n\r\n"
-    assert parser.execute(req, should_be_good)
+    assert_equal req, parser.headers(req, should_be_good)
+    assert_equal '', should_be_good
+    assert parser.keepalive?
+  end
+
+  # legacy test case from Mongrel that we never supported before...
+  # I still consider Pound irrelevant, unfortunately stupid clients that
+  # send extremely big headers do exist and they've managed to find Unicorn...
+  def test_nasty_pound_header
+    parser = HttpParser.new
+    nasty_pound_header = "GET / HTTP/1.1\r\nX-SSL-Bullshit:   -----BEGIN CERTIFICATE-----\r\n\tMIIFbTCCBFWgAwIBAgICH4cwDQYJKoZIhvcNAQEFBQAwcDELMAkGA1UEBhMCVUsx\r\n\tETAPBgNVBAoTCGVTY2llbmNlMRIwEAYDVQQLEwlBdXRob3JpdHkxCzAJBgNVBAMT\r\n\tAkNBMS0wKwYJKoZIhvcNAQkBFh5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMu\r\n\tdWswHhcNMDYwNzI3MTQxMzI4WhcNMDcwNzI3MTQxMzI4WjBbMQswCQYDVQQGEwJV\r\n\tSzERMA8GA1UEChMIZVNjaWVuY2UxEzARBgNVBAsTCk1hbmNoZXN0ZXIxCzAJBgNV\r\n\tBAcTmrsogriqMWLAk1DMRcwFQYDVQQDEw5taWNoYWVsIHBhcmQYJKoZIhvcNAQEB\r\n\tBQADggEPADCCAQoCggEBANPEQBgl1IaKdSS1TbhF3hEXSl72G9J+WC/1R64fAcEF\r\n\tW51rEyFYiIeZGx/BVzwXbeBoNUK41OK65sxGuflMo5gLflbwJtHBRIEKAfVVp3YR\r\n\tgW7cMA/s/XKgL1GEC7rQw8lIZT8RApukCGqOVHSi/F1SiFlPDxuDfmdiNzL31+sL\r\n\t0iwHDdNkGjy5pyBSB8Y79dsSJtCW/iaLB0/n8Sj7HgvvZJ7x0fr+RQjYOUUfrePP\r\n\tu2MSpFyf+9BbC/aXgaZuiCvSR+8Snv3xApQY+fULK/xY8h8Ua51iXoQ5jrgu2SqR\r\n\twgA7BUi3G8LFzMBl8FRCDYGUDy7M6QaHXx1ZWIPWNKsCAwEAAaOCAiQwggIgMAwG\r\n\tA1UdEwEB/wQCMAAwEQYJYIZIAYb4QgEBBAQDAgWgMA4GA1UdDwEB/wQEAwID6DAs\r\n\tBglghkgBhvhCAQ0EHxYdVUsgZS1TY2llbmNlIFVzZXIgQ2VydGlmaWNhdGUwHQYD\r\n\tVR0OBBYEFDTt/sf9PeMaZDHkUIldrDYMNTBZMIGaBgNVHSMEgZIwgY+AFAI4qxGj\r\n\tloCLDdMVKwiljjDastqooXSkcjBwMQswCQYDVQQGEwJVSzERMA8GA1UEChMIZVNj\r\n\taWVuY2UxEjAQBgNVBAsTCUF1dGhvcml0eTELMAkGA1UEAxMCQ0ExLTArBgkqhkiG\r\n\t9w0BCQEWHmNhLW9wZXJhdG9yQGdyaWQtc3VwcG9ydC5hYy51a4IBADApBgNVHRIE\r\n\tIjAggR5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMudWswGQYDVR0gBBIwEDAO\r\n\tBgwrBgEEAdkvAQEBAQYwPQYJYIZIAYb4QgEEBDAWLmh0dHA6Ly9jYS5ncmlkLXN1\r\n\tcHBvcnQuYWMudmT4sopwqlBWsvcHViL2NybC9jYWNybC5jcmwwPQYJYIZIAYb4QgEDBDAWLmh0\r\n\tdHA6Ly9jYS5ncmlkLXN1cHBvcnQuYWMudWsvcHViL2NybC9jYWNybC5jcmwwPwYD\r\n\tVR0fBDgwNjA0oDKgMIYuaHR0cDovL2NhLmdyaWQt5hYy51ay9wdWIv\r\n\tY3JsL2NhY3JsLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAS/U4iiooBENGW/Hwmmd3\r\n\tXCy6Zrt08YjKCzGNjorT98g8uGsqYjSxv/hmi0qlnlHs+k/3Iobc3LjS5AMYr5L8\r\n\tUO7OSkgFFlLHQyC9JzPfmLCAugvzEbyv4Olnsr8hbxF1MbKZoQxUZtMVu29wjfXk\r\n\thTeApBv7eaKCWpSp7MCbvgzm74izKhu3vlDk9w6qVrxePfGgpKPqfHiOoGhFnbTK\r\n\twTC6o2xq5y0qZ03JonF7OJspEd3I5zKY3E+ov7/ZhW6DqT8UFvsAdjvQbXyhV8Eu\r\n\tYhixw1aKEPzNjNowuIseVogKOLXxWI5vAi5HgXdS0/ES5gDGsABo4fqovUKlgop3\r\n\tRA==\r\n\t-----END CERTIFICATE-----\r\n\r\n"
+    req = {}
+    buf = nasty_pound_header.dup
+
+    assert nasty_pound_header =~ /(-----BEGIN .*--END CERTIFICATE-----)/m
+    expect = $1.dup
+    expect.gsub!(/\r\n\t/, ' ')
+    assert_equal req, parser.headers(req, buf)
+    assert_equal '', buf
+    assert_equal expect, req['HTTP_X_SSL_BULLSHIT']
+  end
+
+  def test_continuation_eats_leading_spaces
+    parser = HttpParser.new
+    header = "GET / HTTP/1.1\r\n" \
+             "X-ASDF:      \r\n" \
+             "\t\r\n" \
+             "    \r\n" \
+             "  ASDF\r\n\r\n"
+    req = {}
+    assert_equal req, parser.headers(req, header)
+    assert_equal '', header
+    assert_equal 'ASDF', req['HTTP_X_ASDF']
+  end
+
+  def test_continuation_eats_scattered_leading_spaces
+    parser = HttpParser.new
+    header = "GET / HTTP/1.1\r\n" \
+             "X-ASDF:   hi\r\n" \
+             "    y\r\n" \
+             "\t\r\n" \
+             "       x\r\n" \
+             "  ASDF\r\n\r\n"
+    req = {}
+    assert_equal req, parser.headers(req, header)
+    assert_equal '', header
+    assert_equal 'hi y x ASDF', req['HTTP_X_ASDF']
+  end
+
+  def test_continuation_with_absolute_uri_and_ignored_host_header
+    parser = HttpParser.new
+    header = "GET http://example.com/ HTTP/1.1\r\n" \
+             "Host: \r\n" \
+             "    YHBT.net\r\n" \
+             "\r\n"
+    req = {}
+    assert_equal req, parser.headers(req, header)
+    assert_equal 'example.com', req['HTTP_HOST']
+  end
 
-    # ref: http://thread.gmane.org/gmane.comp.lang.ruby.mongrel.devel/37/focus=45
-    # (note we got 'pen' mixed up with 'pound' in that thread,
-    # but the gist of it is still relevant: these nasty headers are irrelevant
-    #
-    # nasty_pound_header = "GET / HTTP/1.1\r\nX-SSL-Bullshit:   -----BEGIN CERTIFICATE-----\r\n\tMIIFbTCCBFWgAwIBAgICH4cwDQYJKoZIhvcNAQEFBQAwcDELMAkGA1UEBhMCVUsx\r\n\tETAPBgNVBAoTCGVTY2llbmNlMRIwEAYDVQQLEwlBdXRob3JpdHkxCzAJBgNVBAMT\r\n\tAkNBMS0wKwYJKoZIhvcNAQkBFh5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMu\r\n\tdWswHhcNMDYwNzI3MTQxMzI4WhcNMDcwNzI3MTQxMzI4WjBbMQswCQYDVQQGEwJV\r\n\tSzERMA8GA1UEChMIZVNjaWVuY2UxEzARBgNVBAsTCk1hbmNoZXN0ZXIxCzAJBgNV\r\n\tBAcTmrsogriqMWLAk1DMRcwFQYDVQQDEw5taWNoYWVsIHBhcmQYJKoZIhvcNAQEB\r\n\tBQADggEPADCCAQoCggEBANPEQBgl1IaKdSS1TbhF3hEXSl72G9J+WC/1R64fAcEF\r\n\tW51rEyFYiIeZGx/BVzwXbeBoNUK41OK65sxGuflMo5gLflbwJtHBRIEKAfVVp3YR\r\n\tgW7cMA/s/XKgL1GEC7rQw8lIZT8RApukCGqOVHSi/F1SiFlPDxuDfmdiNzL31+sL\r\n\t0iwHDdNkGjy5pyBSB8Y79dsSJtCW/iaLB0/n8Sj7HgvvZJ7x0fr+RQjYOUUfrePP\r\n\tu2MSpFyf+9BbC/aXgaZuiCvSR+8Snv3xApQY+fULK/xY8h8Ua51iXoQ5jrgu2SqR\r\n\twgA7BUi3G8LFzMBl8FRCDYGUDy7M6QaHXx1ZWIPWNKsCAwEAAaOCAiQwggIgMAwG\r\n\tA1UdEwEB/wQCMAAwEQYJYIZIAYb4QgEBBAQDAgWgMA4GA1UdDwEB/wQEAwID6DAs\r\n\tBglghkgBhvhCAQ0EHxYdVUsgZS1TY2llbmNlIFVzZXIgQ2VydGlmaWNhdGUwHQYD\r\n\tVR0OBBYEFDTt/sf9PeMaZDHkUIldrDYMNTBZMIGaBgNVHSMEgZIwgY+AFAI4qxGj\r\n\tloCLDdMVKwiljjDastqooXSkcjBwMQswCQYDVQQGEwJVSzERMA8GA1UEChMIZVNj\r\n\taWVuY2UxEjAQBgNVBAsTCUF1dGhvcml0eTELMAkGA1UEAxMCQ0ExLTArBgkqhkiG\r\n\t9w0BCQEWHmNhLW9wZXJhdG9yQGdyaWQtc3VwcG9ydC5hYy51a4IBADApBgNVHRIE\r\n\tIjAggR5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMudWswGQYDVR0gBBIwEDAO\r\n\tBgwrBgEEAdkvAQEBAQYwPQYJYIZIAYb4QgEEBDAWLmh0dHA6Ly9jYS5ncmlkLXN1\r\n\tcHBvcnQuYWMudmT4sopwqlBWsvcHViL2NybC9jYWNybC5jcmwwPQYJYIZIAYb4QgEDBDAWLmh0\r\n\tdHA6Ly9jYS5ncmlkLXN1cHBvcnQuYWMudWsvcHViL2NybC9jYWNybC5jcmwwPwYD\r\n\tVR0fBDgwNjA0oDKgMIYuaHR0cDovL2NhLmdyaWQt5hYy51ay9wdWIv\r\n\tY3JsL2NhY3JsLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAS/U4iiooBENGW/Hwmmd3\r\n\tXCy6Zrt08YjKCzGNjorT98g8uGsqYjSxv/hmi0qlnlHs+k/3Iobc3LjS5AMYr5L8\r\n\tUO7OSkgFFlLHQyC9JzPfmLCAugvzEbyv4Olnsr8hbxF1MbKZoQxUZtMVu29wjfXk\r\n\thTeApBv7eaKCWpSp7MCbvgzm74izKhu3vlDk9w6qVrxePfGgpKPqfHiOoGhFnbTK\r\n\twTC6o2xq5y0qZ03JonF7OJspEd3I5zKY3E+ov7/ZhW6DqT8UFvsAdjvQbXyhV8Eu\r\n\tYhixw1aKEPzNjNowuIseVogKOLXxWI5vAi5HgXdS0/ES5gDGsABo4fqovUKlgop3\r\n\tRA==\r\n\t-----END CERTIFICATE-----\r\n\r\n"
-    # parser = HttpParser.new
-    # req = {}
-    # assert parser.execute(req, nasty_pound_header, 0)
+  # this may seem to be testing more of an implementation detail, but
+  # it also helps ensure we're safe in the presence of multiple parsers
+  # in case we ever go multithreaded/evented...
+  def test_resumable_continuations
+    nr = 1000
+    req = {}
+    header = "GET / HTTP/1.1\r\n" \
+             "X-ASDF:      \r\n" \
+             "  hello\r\n"
+    tmp = []
+    nr.times { |i|
+      parser = HttpParser.new
+      assert parser.headers(req, "#{header} #{i}\r\n").nil?
+      asdf = req['HTTP_X_ASDF']
+      assert_equal "hello #{i}", asdf
+      tmp << [ parser, asdf ]
+      req.clear
+    }
+    tmp.each_with_index { |(parser, asdf), i|
+      assert_equal req, parser.headers(req, "#{header} #{i}\r\n .\r\n\r\n")
+      assert_equal "hello #{i} .", asdf
+    }
+  end
+
+  def test_invalid_continuation
+    parser = HttpParser.new
+    header = "GET / HTTP/1.1\r\n" \
+             "    y\r\n" \
+             "Host: hello\r\n" \
+             "\r\n"
+    req = {}
+    assert_raises(HttpParserError) { parser.headers(req, header) }
   end
 
   def test_parse_ie6_urls
@@ -103,7 +238,10 @@ class HttpParserTest < Test::Unit::TestCase
       parser = HttpParser.new
       req = {}
       sorta_safe = %(GET #{path} HTTP/1.1\r\n\r\n)
-      assert parser.execute(req, sorta_safe)
+      assert_equal req, parser.headers(req, sorta_safe)
+      assert_equal path, req['REQUEST_URI']
+      assert_equal '', sorta_safe
+      assert parser.keepalive?
     end
   end
   
@@ -112,28 +250,34 @@ class HttpParserTest < Test::Unit::TestCase
     req = {}
     bad_http = "GET / SsUTF/1.1"
 
-    assert_raises(HttpParserError) { parser.execute(req, bad_http) }
+    assert_raises(HttpParserError) { parser.headers(req, bad_http) }
+
+    # make sure we can recover
     parser.reset
-    assert(parser.execute({}, "GET / HTTP/1.0\r\n\r\n"))
+    req.clear
+    assert_equal req, parser.headers(req, "GET / HTTP/1.0\r\n\r\n")
+    assert ! parser.keepalive?
   end
 
   def test_piecemeal
     parser = HttpParser.new
     req = {}
     http = "GET"
-    assert ! parser.execute(req, http)
-    assert_raises(HttpParserError) { parser.execute(req, http) }
-    assert ! parser.execute(req, http << " / HTTP/1.0")
+    assert_nil parser.headers(req, http)
+    assert_nil parser.headers(req, http)
+    assert_nil parser.headers(req, http << " / HTTP/1.0")
     assert_equal '/', req['REQUEST_PATH']
     assert_equal '/', req['REQUEST_URI']
     assert_equal 'GET', req['REQUEST_METHOD']
-    assert ! parser.execute(req, http << "\r\n")
+    assert_nil parser.headers(req, http << "\r\n")
     assert_equal 'HTTP/1.0', req['HTTP_VERSION']
-    assert ! parser.execute(req, http << "\r")
-    assert parser.execute(req, http << "\n")
-    assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL']
+    assert_nil parser.headers(req, http << "\r")
+    assert_equal req, parser.headers(req, http << "\n")
+    assert_equal 'HTTP/1.0', req['SERVER_PROTOCOL']
     assert_nil req['FRAGMENT']
     assert_equal '', req['QUERY_STRING']
+    assert_equal "", http
+    assert ! parser.keepalive?
   end
 
   # not common, but underscores do appear in practice
@@ -141,7 +285,7 @@ class HttpParserTest < Test::Unit::TestCase
     parser = HttpParser.new
     req = {}
     http = "GET http://under_score.example.com/foo?q=bar HTTP/1.0\r\n\r\n"
-    assert parser.execute(req, http)
+    assert_equal req, parser.headers(req, http)
     assert_equal 'http', req['rack.url_scheme']
     assert_equal '/foo?q=bar', req['REQUEST_URI']
     assert_equal '/foo', req['REQUEST_PATH']
@@ -150,13 +294,54 @@ class HttpParserTest < Test::Unit::TestCase
     assert_equal 'under_score.example.com', req['HTTP_HOST']
     assert_equal 'under_score.example.com', req['SERVER_NAME']
     assert_equal '80', req['SERVER_PORT']
+    assert_equal "", http
+    assert ! parser.keepalive?
+  end
+
+  # some dumb clients add users because they're stupid
+  def test_absolute_uri_w_user
+    parser = HttpParser.new
+    req = {}
+    http = "GET http://user%20space@example.com/foo?q=bar HTTP/1.0\r\n\r\n"
+    assert_equal req, parser.headers(req, http)
+    assert_equal 'http', req['rack.url_scheme']
+    assert_equal '/foo?q=bar', req['REQUEST_URI']
+    assert_equal '/foo', req['REQUEST_PATH']
+    assert_equal 'q=bar', req['QUERY_STRING']
+
+    assert_equal 'example.com', req['HTTP_HOST']
+    assert_equal 'example.com', req['SERVER_NAME']
+    assert_equal '80', req['SERVER_PORT']
+    assert_equal "", http
+    assert ! parser.keepalive?
+  end
+
+  # since Mongrel supported anything URI.parse supported, we're stuck
+  # supporting everything URI.parse supports
+  def test_absolute_uri_uri_parse
+    "#{URI::REGEXP::PATTERN::UNRESERVED};:&=+$,".split(//).each do |char|
+      parser = HttpParser.new
+      req = {}
+      http = "GET http://#{char}@example.com/ HTTP/1.0\r\n\r\n"
+      assert_equal req, parser.headers(req, http)
+      assert_equal 'http', req['rack.url_scheme']
+      assert_equal '/', req['REQUEST_URI']
+      assert_equal '/', req['REQUEST_PATH']
+      assert_equal '', req['QUERY_STRING']
+
+      assert_equal 'example.com', req['HTTP_HOST']
+      assert_equal 'example.com', req['SERVER_NAME']
+      assert_equal '80', req['SERVER_PORT']
+      assert_equal "", http
+      assert ! parser.keepalive?
+    end
   end
 
   def test_absolute_uri
     parser = HttpParser.new
     req = {}
     http = "GET http://example.com/foo?q=bar HTTP/1.0\r\n\r\n"
-    assert parser.execute(req, http)
+    assert_equal req, parser.headers(req, http)
     assert_equal 'http', req['rack.url_scheme']
     assert_equal '/foo?q=bar', req['REQUEST_URI']
     assert_equal '/foo', req['REQUEST_PATH']
@@ -165,6 +350,8 @@ class HttpParserTest < Test::Unit::TestCase
     assert_equal 'example.com', req['HTTP_HOST']
     assert_equal 'example.com', req['SERVER_NAME']
     assert_equal '80', req['SERVER_PORT']
+    assert_equal "", http
+    assert ! parser.keepalive?
   end
 
   # X-Forwarded-Proto is not in rfc2616, absolute URIs are, however...
@@ -173,7 +360,7 @@ class HttpParserTest < Test::Unit::TestCase
     req = {}
     http = "GET https://example.com/foo?q=bar HTTP/1.1\r\n" \
            "X-Forwarded-Proto: http\r\n\r\n"
-    assert parser.execute(req, http)
+    assert_equal req, parser.headers(req, http)
     assert_equal 'https', req['rack.url_scheme']
     assert_equal '/foo?q=bar', req['REQUEST_URI']
     assert_equal '/foo', req['REQUEST_PATH']
@@ -182,6 +369,8 @@ class HttpParserTest < Test::Unit::TestCase
     assert_equal 'example.com', req['HTTP_HOST']
     assert_equal 'example.com', req['SERVER_NAME']
     assert_equal '443', req['SERVER_PORT']
+    assert_equal "", http
+    assert parser.keepalive?
   end
 
   # Host: header should be ignored for absolute URIs
@@ -190,7 +379,7 @@ class HttpParserTest < Test::Unit::TestCase
     req = {}
     http = "GET http://example.com:8080/foo?q=bar HTTP/1.2\r\n" \
            "Host: bad.example.com\r\n\r\n"
-    assert parser.execute(req, http)
+    assert_equal req, parser.headers(req, http)
     assert_equal 'http', req['rack.url_scheme']
     assert_equal '/foo?q=bar', req['REQUEST_URI']
     assert_equal '/foo', req['REQUEST_PATH']
@@ -199,6 +388,8 @@ class HttpParserTest < Test::Unit::TestCase
     assert_equal 'example.com:8080', req['HTTP_HOST']
     assert_equal 'example.com', req['SERVER_NAME']
     assert_equal '8080', req['SERVER_PORT']
+    assert_equal "", http
+    assert ! parser.keepalive? # TODO: read HTTP/1.2 when it's final
   end
 
   def test_absolute_uri_with_empty_port
@@ -206,7 +397,7 @@ class HttpParserTest < Test::Unit::TestCase
     req = {}
     http = "GET https://example.com:/foo?q=bar HTTP/1.1\r\n" \
            "Host: bad.example.com\r\n\r\n"
-    assert parser.execute(req, http)
+    assert_equal req, parser.headers(req, http)
     assert_equal 'https', req['rack.url_scheme']
     assert_equal '/foo?q=bar', req['REQUEST_URI']
     assert_equal '/foo', req['REQUEST_PATH']
@@ -215,32 +406,55 @@ class HttpParserTest < Test::Unit::TestCase
     assert_equal 'example.com:', req['HTTP_HOST']
     assert_equal 'example.com', req['SERVER_NAME']
     assert_equal '443', req['SERVER_PORT']
+    assert_equal "", http
+    assert parser.keepalive? # TODO: read HTTP/1.2 when it's final
   end
 
   def test_put_body_oneshot
     parser = HttpParser.new
     req = {}
     http = "PUT / HTTP/1.0\r\nContent-Length: 5\r\n\r\nabcde"
-    assert parser.execute(req, http)
+    assert_equal req, parser.headers(req, http)
     assert_equal '/', req['REQUEST_PATH']
     assert_equal '/', req['REQUEST_URI']
     assert_equal 'PUT', req['REQUEST_METHOD']
     assert_equal 'HTTP/1.0', req['HTTP_VERSION']
-    assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL']
-    assert_equal "abcde", req[:http_body]
+    assert_equal 'HTTP/1.0', req['SERVER_PROTOCOL']
+    assert_equal "abcde", http
+    assert ! parser.keepalive? # TODO: read HTTP/1.2 when it's final
   end
 
   def test_put_body_later
     parser = HttpParser.new
     req = {}
     http = "PUT /l HTTP/1.0\r\nContent-Length: 5\r\n\r\n"
-    assert parser.execute(req, http)
+    assert_equal req, parser.headers(req, http)
     assert_equal '/l', req['REQUEST_PATH']
     assert_equal '/l', req['REQUEST_URI']
     assert_equal 'PUT', req['REQUEST_METHOD']
     assert_equal 'HTTP/1.0', req['HTTP_VERSION']
-    assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL']
-    assert_equal "", req[:http_body]
+    assert_equal 'HTTP/1.0', req['SERVER_PROTOCOL']
+    assert_equal "", http
+    assert ! parser.keepalive? # TODO: read HTTP/1.2 when it's final
+  end
+
+  def test_unknown_methods
+    %w(GETT HEADR XGET XHEAD).each { |m|
+      parser = HttpParser.new
+      req = {}
+      s = "#{m} /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1\r\n\r\n"
+      ok = false
+      assert_nothing_raised do
+        ok = parser.headers(req, s)
+      end
+      assert ok
+      assert_equal '/forums/1/topics/2375?page=1', req['REQUEST_URI']
+      assert_equal 'posts-17408', req['FRAGMENT']
+      assert_equal 'page=1', req['QUERY_STRING']
+      assert_equal "", s
+      assert_equal m, req['REQUEST_METHOD']
+      assert ! parser.keepalive? # TODO: read HTTP/1.2 when it's final
+    }
   end
 
   def test_fragment_in_uri
@@ -249,12 +463,14 @@ class HttpParserTest < Test::Unit::TestCase
     get = "GET /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1\r\n\r\n"
     ok = false
     assert_nothing_raised do
-      ok = parser.execute(req, get)
+      ok = parser.headers(req, get)
     end
     assert ok
     assert_equal '/forums/1/topics/2375?page=1', req['REQUEST_URI']
     assert_equal 'posts-17408', req['FRAGMENT']
     assert_equal 'page=1', req['QUERY_STRING']
+    assert_equal '', get
+    assert parser.keepalive?
   end
 
   # lame random garbage maker
@@ -279,7 +495,7 @@ class HttpParserTest < Test::Unit::TestCase
     10.times do |c|
       get = "GET /#{rand_data(10,120)} HTTP/1.1\r\nX-#{rand_data(1024, 1024+(c*1024))}: Test\r\n\r\n"
       assert_raises Unicorn::HttpParserError do
-        parser.execute({}, get)
+        parser.headers({}, get)
         parser.reset
       end
     end
@@ -288,7 +504,7 @@ class HttpParserTest < Test::Unit::TestCase
     10.times do |c|
       get = "GET /#{rand_data(10,120)} HTTP/1.1\r\nX-Test: #{rand_data(1024, 1024+(c*1024), false)}\r\n\r\n"
       assert_raises Unicorn::HttpParserError do
-        parser.execute({}, get)
+        parser.headers({}, get)
         parser.reset
       end
     end
@@ -297,7 +513,7 @@ class HttpParserTest < Test::Unit::TestCase
     get = "GET /#{rand_data(10,120)} HTTP/1.1\r\n"
     get << "X-Test: test\r\n" * (80 * 1024)
     assert_raises Unicorn::HttpParserError do
-      parser.execute({}, get)
+      parser.headers({}, get)
       parser.reset
     end
 
@@ -305,7 +521,7 @@ class HttpParserTest < Test::Unit::TestCase
     10.times do |c|
       get = "GET #{rand_data(1024, 1024+(c*1024), false)} #{rand_data(1024, 1024+(c*1024), false)}\r\n\r\n"
       assert_raises Unicorn::HttpParserError do
-        parser.execute({}, get)
+        parser.headers({}, get)
         parser.reset
       end
     end
diff --git a/test/unit/test_http_parser_ng.rb b/test/unit/test_http_parser_ng.rb
new file mode 100644
index 0000000..bb61e7f
--- /dev/null
+++ b/test/unit/test_http_parser_ng.rb
@@ -0,0 +1,420 @@
+# -*- encoding: binary -*-
+
+# coding: binary
+require 'test/test_helper'
+require 'digest/md5'
+
+include Unicorn
+
+class HttpParserNgTest < Test::Unit::TestCase
+
+  def setup
+    @parser = HttpParser.new
+  end
+
+  def test_identity_byte_headers
+    req = {}
+    str = "PUT / HTTP/1.1\r\n"
+    str << "Content-Length: 123\r\n"
+    str << "\r"
+    hdr = ""
+    str.each_byte { |byte|
+      assert_nil @parser.headers(req, hdr << byte.chr)
+    }
+    hdr << "\n"
+    assert_equal req.object_id, @parser.headers(req, hdr).object_id
+    assert_equal '123', req['CONTENT_LENGTH']
+    assert_equal 0, hdr.size
+    assert ! @parser.keepalive?
+    assert @parser.headers?
+    assert 123, @parser.content_length
+  end
+
+  def test_identity_step_headers
+    req = {}
+    str = "PUT / HTTP/1.1\r\n"
+    assert ! @parser.headers(req, str)
+    str << "Content-Length: 123\r\n"
+    assert ! @parser.headers(req, str)
+    str << "\r\n"
+    assert_equal req.object_id, @parser.headers(req, str).object_id
+    assert_equal '123', req['CONTENT_LENGTH']
+    assert_equal 0, str.size
+    assert ! @parser.keepalive?
+    assert @parser.headers?
+  end
+
+  def test_identity_oneshot_header
+    req = {}
+    str = "PUT / HTTP/1.1\r\nContent-Length: 123\r\n\r\n"
+    assert_equal req.object_id, @parser.headers(req, str).object_id
+    assert_equal '123', req['CONTENT_LENGTH']
+    assert_equal 0, str.size
+    assert ! @parser.keepalive?
+  end
+
+  def test_identity_oneshot_header_with_body
+    body = ('a' * 123).freeze
+    req = {}
+    str = "PUT / HTTP/1.1\r\n" \
+          "Content-Length: #{body.length}\r\n" \
+          "\r\n#{body}"
+    assert_equal req.object_id, @parser.headers(req, str).object_id
+    assert_equal '123', req['CONTENT_LENGTH']
+    assert_equal 123, str.size
+    assert_equal body, str
+    tmp = ''
+    assert_nil @parser.filter_body(tmp, str)
+    assert_equal 0, str.size
+    assert_equal tmp, body
+    assert_equal "", @parser.filter_body(tmp, str)
+    assert ! @parser.keepalive?
+  end
+
+  def test_identity_oneshot_header_with_body_partial
+    str = "PUT / HTTP/1.1\r\nContent-Length: 123\r\n\r\na"
+    assert_equal Hash, @parser.headers({}, str).class
+    assert_equal 1, str.size
+    assert_equal 'a', str
+    tmp = ''
+    assert_nil @parser.filter_body(tmp, str)
+    assert_equal "", str
+    assert_equal "a", tmp
+    str << ' ' * 122
+    rv = @parser.filter_body(tmp, str)
+    assert_equal 122, tmp.size
+    assert_nil rv
+    assert_equal "", str
+    assert_equal str.object_id, @parser.filter_body(tmp, str).object_id
+    assert ! @parser.keepalive?
+  end
+
+  def test_identity_oneshot_header_with_body_slop
+    str = "PUT / HTTP/1.1\r\nContent-Length: 1\r\n\r\naG"
+    assert_equal Hash, @parser.headers({}, str).class
+    assert_equal 2, str.size
+    assert_equal 'aG', str
+    tmp = ''
+    assert_nil @parser.filter_body(tmp, str)
+    assert_equal "G", str
+    assert_equal "G", @parser.filter_body(tmp, str)
+    assert_equal 1, tmp.size
+    assert_equal "a", tmp
+    assert ! @parser.keepalive?
+  end
+
+  def test_chunked
+    str = "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n"
+    req = {}
+    assert_equal req, @parser.headers(req, str)
+    assert_equal 0, str.size
+    tmp = ""
+    assert_nil @parser.filter_body(tmp, "6")
+    assert_equal 0, tmp.size
+    assert_nil @parser.filter_body(tmp, rv = "\r\n")
+    assert_equal 0, rv.size
+    assert_equal 0, tmp.size
+    tmp = ""
+    assert_nil @parser.filter_body(tmp, "..")
+    assert_equal "..", tmp
+    assert_nil @parser.filter_body(tmp, "abcd\r\n0\r\n")
+    assert_equal "abcd", tmp
+    rv = "PUT"
+    assert_equal rv.object_id, @parser.filter_body(tmp, rv).object_id
+    assert_equal "PUT", rv
+    assert ! @parser.keepalive?
+  end
+
+  def test_two_chunks
+    str = "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n"
+    req = {}
+    assert_equal req, @parser.headers(req, str)
+    assert_equal 0, str.size
+    tmp = ""
+    assert_nil @parser.filter_body(tmp, "6")
+    assert_equal 0, tmp.size
+    assert_nil @parser.filter_body(tmp, rv = "\r\n")
+    assert_equal "", rv
+    assert_equal 0, tmp.size
+    tmp = ""
+    assert_nil @parser.filter_body(tmp, "..")
+    assert_equal 2, tmp.size
+    assert_equal "..", tmp
+    assert_nil @parser.filter_body(tmp, "abcd\r\n1")
+    assert_equal "abcd", tmp
+    assert_nil @parser.filter_body(tmp, "\r")
+    assert_equal "", tmp
+    assert_nil @parser.filter_body(tmp, "\n")
+    assert_equal "", tmp
+    assert_nil @parser.filter_body(tmp, "z")
+    assert_equal "z", tmp
+    assert_nil @parser.filter_body(tmp, "\r\n")
+    assert_nil @parser.filter_body(tmp, "0")
+    assert_nil @parser.filter_body(tmp, "\r")
+    rv = @parser.filter_body(tmp, buf = "\nGET")
+    assert_equal "GET", rv
+    assert_equal buf.object_id, rv.object_id
+    assert ! @parser.keepalive?
+  end
+
+  def test_big_chunk
+    str = "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n" \
+          "4000\r\nabcd"
+    req = {}
+    assert_equal req, @parser.headers(req, str)
+    tmp = ''
+    assert_nil @parser.filter_body(tmp, str)
+    assert_equal '', str
+    str = ' ' * 16300
+    assert_nil @parser.filter_body(tmp, str)
+    assert_equal '', str
+    str = ' ' * 80
+    assert_nil @parser.filter_body(tmp, str)
+    assert_equal '', str
+    assert ! @parser.body_eof?
+    assert_equal "", @parser.filter_body(tmp, "\r\n0\r\n")
+    assert @parser.body_eof?
+    assert ! @parser.keepalive?
+  end
+
+  def test_two_chunks_oneshot
+    str = "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n" \
+          "1\r\na\r\n2\r\n..\r\n0\r\n"
+    req = {}
+    assert_equal req, @parser.headers(req, str)
+    tmp = ''
+    assert_nil @parser.filter_body(tmp, str)
+    assert_equal 'a..', tmp
+    rv = @parser.filter_body(tmp, str)
+    assert_equal rv.object_id, str.object_id
+    assert ! @parser.keepalive?
+  end
+
+  def test_chunks_bytewise
+    chunked = "10\r\nabcdefghijklmnop\r\n11\r\n0123456789abcdefg\r\n0\r\n"
+    str = "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n#{chunked}"
+    req = {}
+    assert_equal req, @parser.headers(req, str)
+    assert_equal chunked, str
+    tmp = ''
+    buf = ''
+    body = ''
+    str = str[0..-2]
+    str.each_byte { |byte|
+      assert_nil @parser.filter_body(tmp, buf << byte.chr)
+      body << tmp
+    }
+    assert_equal 'abcdefghijklmnop0123456789abcdefg', body
+    rv = @parser.filter_body(tmp, buf << "\n")
+    assert_equal rv.object_id, buf.object_id
+    assert ! @parser.keepalive?
+  end
+
+  def test_trailers
+    str = "PUT / HTTP/1.1\r\n" \
+          "Trailer: Content-MD5\r\n" \
+          "transfer-Encoding: chunked\r\n\r\n" \
+          "1\r\na\r\n2\r\n..\r\n0\r\n"
+    req = {}
+    assert_equal req, @parser.headers(req, str)
+    assert_equal 'Content-MD5', req['HTTP_TRAILER']
+    assert_nil req['HTTP_CONTENT_MD5']
+    tmp = ''
+    assert_nil @parser.filter_body(tmp, str)
+    assert_equal 'a..', tmp
+    md5_b64 = [ Digest::MD5.digest(tmp) ].pack('m').strip.freeze
+    rv = @parser.filter_body(tmp, str)
+    assert_equal rv.object_id, str.object_id
+    assert_equal '', str
+    md5_hdr = "Content-MD5: #{md5_b64}\r\n".freeze
+    str << md5_hdr
+    assert_nil @parser.trailers(req, str)
+    assert_equal md5_b64, req['HTTP_CONTENT_MD5']
+    assert_equal "CONTENT_MD5: #{md5_b64}\r\n", str
+    assert_nil @parser.trailers(req, str << "\r")
+    assert_equal req, @parser.trailers(req, str << "\nGET / ")
+    assert_equal "GET / ", str
+    assert ! @parser.keepalive?
+  end
+
+  def test_trailers_slowly
+    str = "PUT / HTTP/1.1\r\n" \
+          "Trailer: Content-MD5\r\n" \
+          "transfer-Encoding: chunked\r\n\r\n" \
+          "1\r\na\r\n2\r\n..\r\n0\r\n"
+    req = {}
+    assert_equal req, @parser.headers(req, str)
+    assert_equal 'Content-MD5', req['HTTP_TRAILER']
+    assert_nil req['HTTP_CONTENT_MD5']
+    tmp = ''
+    assert_nil @parser.filter_body(tmp, str)
+    assert_equal 'a..', tmp
+    md5_b64 = [ Digest::MD5.digest(tmp) ].pack('m').strip.freeze
+    rv = @parser.filter_body(tmp, str)
+    assert_equal rv.object_id, str.object_id
+    assert_equal '', str
+    assert_nil @parser.trailers(req, str)
+    md5_hdr = "Content-MD5: #{md5_b64}\r\n".freeze
+    md5_hdr.each_byte { |byte|
+      str << byte.chr
+      assert_nil @parser.trailers(req, str)
+    }
+    assert_equal md5_b64, req['HTTP_CONTENT_MD5']
+    assert_equal "CONTENT_MD5: #{md5_b64}\r\n", str
+    assert_nil @parser.trailers(req, str << "\r")
+    assert_equal req, @parser.trailers(req, str << "\n")
+  end
+
+  def test_max_chunk
+    str = "PUT / HTTP/1.1\r\n" \
+          "transfer-Encoding: chunked\r\n\r\n" \
+          "#{HttpParser::CHUNK_MAX.to_s(16)}\r\na\r\n2\r\n..\r\n0\r\n"
+    req = {}
+    assert_equal req, @parser.headers(req, str)
+    assert_nil @parser.content_length
+    assert_nothing_raised { @parser.filter_body('', str) }
+    assert ! @parser.keepalive?
+  end
+
+  def test_max_body
+    n = HttpParser::LENGTH_MAX
+    str = "PUT / HTTP/1.1\r\nContent-Length: #{n}\r\n\r\n"
+    req = {}
+    assert_nothing_raised { @parser.headers(req, str) }
+    assert_equal n, req['CONTENT_LENGTH'].to_i
+    assert ! @parser.keepalive?
+  end
+
+  def test_overflow_chunk
+    n = HttpParser::CHUNK_MAX + 1
+    str = "PUT / HTTP/1.1\r\n" \
+          "transfer-Encoding: chunked\r\n\r\n" \
+          "#{n.to_s(16)}\r\na\r\n2\r\n..\r\n0\r\n"
+    req = {}
+    assert_equal req, @parser.headers(req, str)
+    assert_nil @parser.content_length
+    assert_raise(HttpParserError) { @parser.filter_body('', str) }
+    assert ! @parser.keepalive?
+  end
+
+  def test_overflow_content_length
+    n = HttpParser::LENGTH_MAX + 1
+    str = "PUT / HTTP/1.1\r\nContent-Length: #{n}\r\n\r\n"
+    assert_raise(HttpParserError) { @parser.headers({}, str) }
+    assert ! @parser.keepalive?
+  end
+
+  def test_bad_chunk
+    str = "PUT / HTTP/1.1\r\n" \
+          "transfer-Encoding: chunked\r\n\r\n" \
+          "#zzz\r\na\r\n2\r\n..\r\n0\r\n"
+    req = {}
+    assert_equal req, @parser.headers(req, str)
+    assert_nil @parser.content_length
+    assert_raise(HttpParserError) { @parser.filter_body('', str) }
+    assert ! @parser.keepalive?
+  end
+
+  def test_bad_content_length
+    str = "PUT / HTTP/1.1\r\nContent-Length: 7ff\r\n\r\n"
+    assert_raise(HttpParserError) { @parser.headers({}, str) }
+    assert ! @parser.keepalive?
+  end
+
+  def test_bad_trailers
+    str = "PUT / HTTP/1.1\r\n" \
+          "Trailer: Transfer-Encoding\r\n" \
+          "transfer-Encoding: chunked\r\n\r\n" \
+          "1\r\na\r\n2\r\n..\r\n0\r\n"
+    req = {}
+    assert_equal req, @parser.headers(req, str)
+    assert_equal 'Transfer-Encoding', req['HTTP_TRAILER']
+    tmp = ''
+    assert_nil @parser.filter_body(tmp, str)
+    assert_equal 'a..', tmp
+    assert_equal '', str
+    str << "Transfer-Encoding: identity\r\n\r\n"
+    assert_raise(HttpParserError) { @parser.trailers(req, str) }
+    assert ! @parser.keepalive?
+  end
+
+  def test_repeat_headers
+    str = "PUT / HTTP/1.1\r\n" \
+          "Trailer: Content-MD5\r\n" \
+          "Trailer: Content-SHA1\r\n" \
+          "transfer-Encoding: chunked\r\n\r\n" \
+          "1\r\na\r\n2\r\n..\r\n0\r\n"
+    req = {}
+    assert_equal req, @parser.headers(req, str)
+    assert_equal 'Content-MD5,Content-SHA1', req['HTTP_TRAILER']
+    assert ! @parser.keepalive?
+  end
+
+  def test_parse_simple_request
+    parser = HttpParser.new
+    req = {}
+    http = "GET /read-rfc1945-if-you-dont-believe-me\r\n"
+    assert_equal req, parser.headers(req, http)
+    assert_equal '', http
+    expect = {
+      "SERVER_NAME"=>"localhost",
+      "rack.url_scheme"=>"http",
+      "REQUEST_PATH"=>"/read-rfc1945-if-you-dont-believe-me",
+      "PATH_INFO"=>"/read-rfc1945-if-you-dont-believe-me",
+      "REQUEST_URI"=>"/read-rfc1945-if-you-dont-believe-me",
+      "SERVER_PORT"=>"80",
+      "SERVER_PROTOCOL"=>"HTTP/0.9",
+      "REQUEST_METHOD"=>"GET",
+      "QUERY_STRING"=>""
+    }
+    assert_equal expect, req
+    assert ! parser.headers?
+  end
+
+  def test_path_info_semicolon
+    qs = "QUERY_STRING"
+    pi = "PATH_INFO"
+    req = {}
+    str = "GET %s HTTP/1.1\r\nHost: example.com\r\n\r\n"
+    {
+      "/1;a=b?c=d&e=f" => { qs => "c=d&e=f", pi => "/1;a=b" },
+      "/1?c=d&e=f" => { qs => "c=d&e=f", pi => "/1" },
+      "/1;a=b" => { qs => "", pi => "/1;a=b" },
+      "/1;a=b?" => { qs => "", pi => "/1;a=b" },
+      "/1?a=b;c=d&e=f" => { qs => "a=b;c=d&e=f", pi => "/1" },
+      "*" => { qs => "", pi => "" },
+    }.each do |uri,expect|
+      assert_equal req, @parser.headers(req.clear, str % [ uri ])
+      @parser.reset
+      assert_equal uri, req["REQUEST_URI"], "REQUEST_URI mismatch"
+      assert_equal expect[qs], req[qs], "#{qs} mismatch"
+      assert_equal expect[pi], req[pi], "#{pi} mismatch"
+      next if uri == "*"
+      uri = URI.parse("http://example.com#{uri}")
+      assert_equal uri.query.to_s, req[qs], "#{qs} mismatch URI.parse disagrees"
+      assert_equal uri.path, req[pi], "#{pi} mismatch URI.parse disagrees"
+    end
+  end
+
+  def test_path_info_semicolon_absolute
+    qs = "QUERY_STRING"
+    pi = "PATH_INFO"
+    req = {}
+    str = "GET http://example.com%s HTTP/1.1\r\nHost: www.example.com\r\n\r\n"
+    {
+      "/1;a=b?c=d&e=f" => { qs => "c=d&e=f", pi => "/1;a=b" },
+      "/1?c=d&e=f" => { qs => "c=d&e=f", pi => "/1" },
+      "/1;a=b" => { qs => "", pi => "/1;a=b" },
+      "/1;a=b?" => { qs => "", pi => "/1;a=b" },
+      "/1?a=b;c=d&e=f" => { qs => "a=b;c=d&e=f", pi => "/1" },
+    }.each do |uri,expect|
+      assert_equal req, @parser.headers(req.clear, str % [ uri ])
+      @parser.reset
+      assert_equal uri, req["REQUEST_URI"], "REQUEST_URI mismatch"
+      assert_equal "example.com", req["HTTP_HOST"], "Host: mismatch"
+      assert_equal expect[qs], req[qs], "#{qs} mismatch"
+      assert_equal expect[pi], req[pi], "#{pi} mismatch"
+    end
+  end
+
+end
diff --git a/test/unit/test_request.rb b/test/unit/test_request.rb
index 0bfff7d..1896300 100644
--- a/test/unit/test_request.rb
+++ b/test/unit/test_request.rb
@@ -1,14 +1,9 @@
+# -*- encoding: binary -*-
+
 # Copyright (c) 2009 Eric Wong
 # You can redistribute it and/or modify it under the same terms as Ruby.
 
 require 'test/test_helper'
-begin
-  require 'rack'
-  require 'rack/lint'
-rescue LoadError
-  warn "Unable to load rack, skipping test"
-  exit 0
-end
 
 include Unicorn
 
@@ -16,10 +11,11 @@ class RequestTest < Test::Unit::TestCase
 
   class MockRequest < StringIO
     alias_method :readpartial, :sysread
+    alias_method :read_nonblock, :sysread
   end
 
   def setup
-    @request = HttpRequest.new(Logger.new($stderr))
+    @request = HttpRequest.new
     @app = lambda do |env|
       [ 200, { 'Content-Length' => '0', 'Content-Type' => 'text/plain' }, [] ]
     end
@@ -119,6 +115,31 @@ class RequestTest < Test::Unit::TestCase
     assert_nothing_raised { res = @lint.call(env) }
   end
 
+  def test_no_content_stringio
+    client = MockRequest.new("GET / HTTP/1.1\r\nHost: foo\r\n\r\n")
+    res = env = nil
+    assert_nothing_raised { env = @request.read(client) }
+    assert_equal StringIO, env['rack.input'].class
+  end
+
+  def test_zero_content_stringio
+    client = MockRequest.new("PUT / HTTP/1.1\r\n" \
+                             "Content-Length: 0\r\n" \
+                             "Host: foo\r\n\r\n")
+    res = env = nil
+    assert_nothing_raised { env = @request.read(client) }
+    assert_equal StringIO, env['rack.input'].class
+  end
+
+  def test_real_content_not_stringio
+    client = MockRequest.new("PUT / HTTP/1.1\r\n" \
+                             "Content-Length: 1\r\n" \
+                             "Host: foo\r\n\r\n")
+    res = env = nil
+    assert_nothing_raised { env = @request.read(client) }
+    assert_equal Unicorn::TeeInput, env['rack.input'].class
+  end
+
   def test_rack_lint_put
     client = MockRequest.new(
       "PUT / HTTP/1.1\r\n" \
@@ -149,7 +170,11 @@ class RequestTest < Test::Unit::TestCase
     assert_nothing_raised { env = @request.read(client) }
     assert ! env.include?(:http_body)
     assert_equal length, env['rack.input'].size
-    count.times { assert_equal buf, env['rack.input'].read(bs) }
+    count.times {
+      tmp = env['rack.input'].read(bs)
+      tmp << env['rack.input'].read(bs - tmp.size) if tmp.size != bs
+      assert_equal buf, tmp
+    }
     assert_nil env['rack.input'].read(bs)
     assert_nothing_raised { env['rack.input'].rewind }
     assert_nothing_raised { res = @lint.call(env) }
diff --git a/test/unit/test_response.rb b/test/unit/test_response.rb
index 66c2b54..f9eda8e 100644
--- a/test/unit/test_response.rb
+++ b/test/unit/test_response.rb
@@ -1,3 +1,5 @@
+# -*- encoding: binary -*-
+
 # Copyright (c) 2005 Zed A. Shaw
 # You can redistribute it and/or modify it under the same terms as Ruby.
 #
@@ -94,4 +96,15 @@ class ResponseTest < Test::Unit::TestCase
     assert_match(expect_body, out.string.split(/\r\n/).last)
   end
 
+  def test_unknown_status_pass_through
+    out = StringIO.new
+    HttpResponse.write(out,["666 I AM THE BEAST", {}, [] ])
+    assert out.closed?
+    headers = out.string.split(/\r\n\r\n/).first.split(/\r\n/)
+    assert %r{\AHTTP/\d\.\d 666 I AM THE BEAST\z}.match(headers[0])
+    status = headers.grep(/\AStatus:/i).first
+    assert status
+    assert_equal "Status: 666 I AM THE BEAST", status
+  end
+
 end
diff --git a/test/unit/test_server.rb b/test/unit/test_server.rb
index 742b240..00705d0 100644
--- a/test/unit/test_server.rb
+++ b/test/unit/test_server.rb
@@ -1,3 +1,5 @@
+# -*- encoding: binary -*-
+
 # Copyright (c) 2005 Zed A. Shaw
 # You can redistribute it and/or modify it under the same terms as Ruby.
 #
@@ -10,9 +12,13 @@ include Unicorn
 
 class TestHandler
 
-  def call(env)
-  #   response.socket.write("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nhello!\n")
+  def call(env)
+    while env['rack.input'].read(4096)
+    end
     [200, { 'Content-Type' => 'text/plain' }, ['hello!\n']]
+    rescue Unicorn::ClientShutdown, Unicorn::HttpParserError => e
+      $stderr.syswrite("#{e.class}: #{e.message} #{e.backtrace.empty?}\n")
+      raise e
    end
 end
 
@@ -31,6 +37,8 @@ class WebServerTest < Test::Unit::TestCase
 
   def teardown
     redirect_test_io do
+      wait_workers_ready("test_stderr.#$$.log", 1)
+      File.truncate("test_stderr.#$$.log", 0)
       @server.stop(true)
     end
   end
@@ -51,8 +59,10 @@ class WebServerTest < Test::Unit::TestCase
     end
     results = hit(["http://localhost:#@port/"])
     worker_pid = results[0].to_i
+    assert worker_pid != 0
     tmp.sysseek(0)
     loader_pid = tmp.sysread(4096).to_i
+    assert loader_pid != 0
     assert_equal worker_pid, loader_pid
     teardown
 
@@ -63,6 +73,7 @@ class WebServerTest < Test::Unit::TestCase
     end
     results = hit(["http://localhost:#@port/"])
     worker_pid = results[0].to_i
+    assert worker_pid != 0
     tmp.sysseek(0)
     loader_pid = tmp.sysread(4096).to_i
     assert_equal $$, loader_pid
@@ -94,6 +105,92 @@ class WebServerTest < Test::Unit::TestCase
     assert_equal 'hello!\n', results[0], "Handler didn't really run"
   end
 
+  def test_client_shutdown_writes
+    sock = nil
+    buf = nil
+    bs = 15609315 * rand
+    assert_nothing_raised do
+      sock = TCPSocket.new('127.0.0.1', @port)
+      sock.syswrite("PUT /hello HTTP/1.1\r\n")
+      sock.syswrite("Host: example.com\r\n")
+      sock.syswrite("Transfer-Encoding: chunked\r\n")
+      sock.syswrite("Trailer: X-Foo\r\n")
+      sock.syswrite("\r\n")
+      sock.syswrite("%x\r\n" % [ bs ])
+      sock.syswrite("F" * bs)
+      sock.syswrite("\r\n0\r\nX-")
+      "Foo: bar\r\n\r\n".each_byte do |x|
+        sock.syswrite x.chr
+        sleep 0.05
+      end
+      # we wrote the entire request before shutting down, server should
+      # continue to process our request and never hit EOFError on our sock
+      sock.shutdown(Socket::SHUT_WR)
+      buf = sock.read
+    end
+    assert_equal 'hello!\n', buf.split(/\r\n\r\n/).last
+    next_client = Net::HTTP.get(URI.parse("http://127.0.0.1:#@port/"))
+    assert_equal 'hello!\n', next_client
+    lines = File.readlines("test_stderr.#$$.log")
+    assert lines.grep(/^Unicorn::ClientShutdown: /).empty?
+    assert_nothing_raised { sock.close }
+  end
+
+  def test_client_shutdown_write_truncates
+    sock = nil
+    buf = nil
+    bs = 15609315 * rand
+    assert_nothing_raised do
+      sock = TCPSocket.new('127.0.0.1', @port)
+      sock.syswrite("PUT /hello HTTP/1.1\r\n")
+      sock.syswrite("Host: example.com\r\n")
+      sock.syswrite("Transfer-Encoding: chunked\r\n")
+      sock.syswrite("Trailer: X-Foo\r\n")
+      sock.syswrite("\r\n")
+      sock.syswrite("%x\r\n" % [ bs ])
+      sock.syswrite("F" * (bs / 2.0))
+
+      # shutdown prematurely, this will force the server to abort
+      # processing on us even during app dispatch
+      sock.shutdown(Socket::SHUT_WR)
+      IO.select([sock], nil, nil, 60) or raise "Timed out"
+      buf = sock.read
+    end
+    assert_equal "", buf
+    next_client = Net::HTTP.get(URI.parse("http://127.0.0.1:#@port/"))
+    assert_equal 'hello!\n', next_client
+    lines = File.readlines("test_stderr.#$$.log")
+    lines = lines.grep(/^Unicorn::ClientShutdown: bytes_read=\d+/)
+    assert_equal 1, lines.size
+    assert_match %r{\AUnicorn::ClientShutdown: bytes_read=\d+ true$}, lines[0]
+    assert_nothing_raised { sock.close }
+  end
+
+  def test_client_malformed_body
+    sock = nil
+    buf = nil
+    bs = 15653984
+    assert_nothing_raised do
+      sock = TCPSocket.new('127.0.0.1', @port)
+      sock.syswrite("PUT /hello HTTP/1.1\r\n")
+      sock.syswrite("Host: example.com\r\n")
+      sock.syswrite("Transfer-Encoding: chunked\r\n")
+      sock.syswrite("Trailer: X-Foo\r\n")
+      sock.syswrite("\r\n")
+      sock.syswrite("%x\r\n" % [ bs ])
+      sock.syswrite("F" * bs)
+    end
+    begin
+      File.open("/dev/urandom", "rb") { |fp| sock.syswrite(fp.sysread(16384)) }
+    rescue
+    end
+    assert_nothing_raised { sock.close }
+    next_client = Net::HTTP.get(URI.parse("http://127.0.0.1:#@port/"))
+    assert_equal 'hello!\n', next_client
+    lines = File.readlines("test_stderr.#$$.log")
+    lines = lines.grep(/^Unicorn::HttpParserError: .* true$/)
+    assert_equal 1, lines.size
+  end
 
   def do_test(string, chunk, close_after=nil, shutdown_delay=0)
     # Do not use instance variables here, because it needs to be thread safe
@@ -131,6 +228,16 @@ class WebServerTest < Test::Unit::TestCase
     end
   end
 
+  def test_logger_set
+    assert_equal @server.logger, Unicorn::HttpRequest::DEFAULTS["rack.logger"]
+  end
+
+  def test_logger_changed
+    tmp = Logger.new($stdout)
+    @server.logger = tmp
+    assert_equal tmp, Unicorn::HttpRequest::DEFAULTS["rack.logger"]
+  end
+
   def test_bad_client_400
     sock = nil
     assert_nothing_raised do
@@ -141,6 +248,16 @@ class WebServerTest < Test::Unit::TestCase
     assert_nothing_raised { sock.close }
   end
 
+  def test_http_0_9
+    sock = nil
+    assert_nothing_raised do
+      sock = TCPSocket.new('127.0.0.1', @port)
+      sock.syswrite("GET /hello\r\n")
+    end
+    assert_match 'hello!\n', sock.sysread(4096)
+    assert_nothing_raised { sock.close }
+  end
+
   def test_header_is_too_long
     redirect_test_io do
       long = "GET /test HTTP/1.1\r\n" + ("X-Big: stuff\r\n" * 15000) + "\r\n"
@@ -152,9 +269,18 @@ class WebServerTest < Test::Unit::TestCase
 
   def test_file_streamed_request
     body = "a" * (Unicorn::Const::MAX_BODY * 2)
-    long = "GET /test HTTP/1.1\r\nContent-length: #{body.length}\r\n\r\n" + body
+    long = "PUT /test HTTP/1.1\r\nContent-length: #{body.length}\r\n\r\n" + body
     do_test(long, Unicorn::Const::CHUNK_SIZE * 2 -400)
   end
 
+  def test_file_streamed_request_bad_body
+    body = "a" * (Unicorn::Const::MAX_BODY * 2)
+    long = "GET /test HTTP/1.1\r\nContent-ength: #{body.length}\r\n\r\n" + body
+    assert_raises(EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,
+                  Errno::EBADF) {
+      do_test(long, Unicorn::Const::CHUNK_SIZE * 2 -400)
+    }
+  end
+
 end
 
diff --git a/test/unit/test_signals.rb b/test/unit/test_signals.rb
index ef66ed6..eb2af0b 100644
--- a/test/unit/test_signals.rb
+++ b/test/unit/test_signals.rb
@@ -1,3 +1,5 @@
+# -*- encoding: binary -*-
+
 # Copyright (c) 2009 Eric Wong
 # You can redistribute it and/or modify it under the same terms as Ruby.
 #
@@ -24,14 +26,15 @@ class SignalsTest < Test::Unit::TestCase
     @bs = 1 * 1024 * 1024
     @count = 100
     @port = unused_port
-    tmp = @tmp = Tempfile.new('unicorn.sock')
+    @sock = Tempfile.new('unicorn.sock')
+    @tmp = Tempfile.new('unicorn.write')
+    @tmp.sync = true
+    File.unlink(@sock.path)
     File.unlink(@tmp.path)
-    n = 0
-    tmp.chmod(0)
     @server_opts = {
-      :listeners => [ "127.0.0.1:#@port", @tmp.path ],
+      :listeners => [ "127.0.0.1:#@port", @sock.path ],
       :after_fork => lambda { |server,worker|
-        trap(:HUP) { tmp.chmod(n += 1) }
+        trap(:HUP) { @tmp.syswrite('.') }
       },
     }
     @server = nil
@@ -53,8 +56,10 @@ class SignalsTest < Test::Unit::TestCase
       buf =~ /\bX-Pid: (\d+)\b/ or raise Exception
       child = $1.to_i
       wait_master_ready("test_stderr.#{pid}.log")
+      wait_workers_ready("test_stderr.#{pid}.log", 1)
       Process.kill(:KILL, pid)
       Process.waitpid(pid)
+      File.unlink("test_stderr.#{pid}.log", "test_stdout.#{pid}.log")
       t0 = Time.now
     end
     assert child
@@ -137,8 +142,9 @@ class SignalsTest < Test::Unit::TestCase
       pid = buf[/\r\nX-Pid: (\d+)\r\n/, 1].to_i
       header_len = buf[/\A(.+?\r\n\r\n)/m, 1].size
     end
+    assert pid > 0, "pid not positive: #{pid.inspect}"
     read = buf.size
-    mode_before = @tmp.stat.mode
+    size_before = @tmp.stat.size
     assert_raises(EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,
                   Errno::EBADF) do
       loop do
@@ -151,13 +157,17 @@ class SignalsTest < Test::Unit::TestCase
 
     redirect_test_io { @server.stop(true) }
     # can't check for == since pending signals get merged
-    assert mode_before < @tmp.stat.mode
-    assert_equal(read - header_len, @bs * @count)
+    assert size_before < @tmp.stat.size
+    got = read - header_len
+    expect = @bs * @count
+    assert_equal(expect, got, "expect=#{expect} got=#{got}")
     assert_nothing_raised { sock.close }
   end
 
   def test_request_read
     app = lambda { |env|
+      while env['rack.input'].read(4096)
+      end
       [ 200, {'Content-Type'=>'text/plain', 'X-Pid'=>Process.pid.to_s}, [] ]
     }
     redirect_test_io { @server = HttpServer.new(app, @server_opts).start }
@@ -171,11 +181,12 @@ class SignalsTest < Test::Unit::TestCase
       sock.close
     end
 
+    assert pid > 0, "pid not positive: #{pid.inspect}"
     sock = TCPSocket.new('127.0.0.1', @port)
     sock.syswrite("PUT / HTTP/1.0\r\n")
     sock.syswrite("Content-Length: #{@bs * @count}\r\n\r\n")
     1000.times { Process.kill(:HUP, pid) }
-    mode_before = @tmp.stat.mode
+    size_before = @tmp.stat.size
     killer = fork { loop { Process.kill(:HUP, pid); sleep(0.0001) } }
     buf = ' ' * @bs
     @count.times { sock.syswrite(buf) }
@@ -183,7 +194,7 @@ class SignalsTest < Test::Unit::TestCase
     Process.waitpid2(killer)
     redirect_test_io { @server.stop(true) }
     # can't check for == since pending signals get merged
-    assert mode_before < @tmp.stat.mode
+    assert size_before < @tmp.stat.size
     assert_equal pid, sock.sysread(4096)[/\r\nX-Pid: (\d+)\r\n/, 1].to_i
     sock.close
   end
diff --git a/test/unit/test_socket_helper.rb b/test/unit/test_socket_helper.rb
index 75d9f7b..c35b0c2 100644
--- a/test/unit/test_socket_helper.rb
+++ b/test/unit/test_socket_helper.rb
@@ -1,3 +1,5 @@
+# -*- encoding: binary -*-
+
 require 'test/test_helper'
 require 'tempfile'
 
@@ -61,6 +63,20 @@ class TestSocketHelper < Test::Unit::TestCase
       File.umask(old_umask)
   end
 
+  def test_bind_listen_unix_umask
+    old_umask = File.umask(0777)
+    tmp = Tempfile.new 'unix.sock'
+    @unix_listener_path = tmp.path
+    File.unlink(@unix_listener_path)
+    @unix_listener = bind_listen(@unix_listener_path, :umask => 077)
+    assert UNIXServer === @unix_listener
+    assert_equal @unix_listener_path, sock_name(@unix_listener)
+    assert_equal 0140700, File.stat(@unix_listener_path).mode
+    assert_equal 0777, File.umask
+    ensure
+      File.umask(old_umask)
+  end
+
   def test_bind_listen_unix_idempotent
     test_bind_listen_unix
     a = bind_listen(@unix_listener)
diff --git a/test/unit/test_tee_input.rb b/test/unit/test_tee_input.rb
new file mode 100644
index 0000000..403f698
--- /dev/null
+++ b/test/unit/test_tee_input.rb
@@ -0,0 +1,229 @@
+# -*- encoding: binary -*-
+
+require 'test/unit'
+require 'digest/sha1'
+require 'unicorn'
+
+class TestTeeInput < Test::Unit::TestCase
+
+  def setup
+    @rs = $/
+    @env = {}
+    @rd, @wr = IO.pipe
+    @rd.sync = @wr.sync = true
+    @start_pid = $$
+  end
+
+  def teardown
+    return if $$ != @start_pid
+    $/ = @rs
+    @rd.close rescue nil
+    @wr.close rescue nil
+    begin
+      Process.wait
+    rescue Errno::ECHILD
+      break
+    end while true
+  end
+
+  def test_gets_long
+    init_parser("hello", 5 + (4096 * 4 * 3) + "#$/foo#$/".size)
+    ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf)
+    status = line = nil
+    pid = fork {
+      @rd.close
+      3.times { @wr.write("ffff" * 4096) }
+      @wr.write "#$/foo#$/"
+      @wr.close
+    }
+    @wr.close
+    assert_nothing_raised { line = ti.gets }
+    assert_equal(4096 * 4 * 3 + 5 + $/.size, line.size)
+    assert_equal("hello" << ("ffff" * 4096 * 3) << "#$/", line)
+    assert_nothing_raised { line = ti.gets }
+    assert_equal "foo#$/", line
+    assert_nil ti.gets
+    assert_nothing_raised { pid, status = Process.waitpid2(pid) }
+    assert status.success?
+  end
+
+  def test_gets_short
+    init_parser("hello", 5 + "#$/foo".size)
+    ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf)
+    status = line = nil
+    pid = fork {
+      @rd.close
+      @wr.write "#$/foo"
+      @wr.close
+    }
+    @wr.close
+    assert_nothing_raised { line = ti.gets }
+    assert_equal("hello#$/", line)
+    assert_nothing_raised { line = ti.gets }
+    assert_equal "foo", line
+    assert_nil ti.gets
+    assert_nothing_raised { pid, status = Process.waitpid2(pid) }
+    assert status.success?
+  end
+
+  def test_small_body
+    init_parser('hello')
+    ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf)
+    assert_equal 0, @parser.content_length
+    assert @parser.body_eof?
+    assert_equal StringIO, ti.instance_eval { @tmp.class }
+    assert_equal 0, ti.instance_eval { @tmp.pos }
+    assert_equal 5, ti.size
+    assert_equal 'hello', ti.read
+    assert_equal '', ti.read
+    assert_nil ti.read(4096)
+  end
+
+  def test_read_with_buffer
+    init_parser('hello')
+    ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf)
+    buf = ''
+    rv = ti.read(4, buf)
+    assert_equal 'hell', rv
+    assert_equal 'hell', buf
+    assert_equal rv.object_id, buf.object_id
+    assert_equal 'o', ti.read
+    assert_equal nil, ti.read(5, buf)
+    assert_equal 0, ti.rewind
+    assert_equal 'hello', ti.read(5, buf)
+    assert_equal 'hello', buf
+  end
+
+  def test_big_body
+    init_parser('.' * Unicorn::Const::MAX_BODY << 'a')
+    ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf)
+    assert_equal 0, @parser.content_length
+    assert @parser.body_eof?
+    assert_kind_of File, ti.instance_eval { @tmp }
+    assert_equal 0, ti.instance_eval { @tmp.pos }
+    assert_equal Unicorn::Const::MAX_BODY + 1, ti.size
+  end
+
+  def test_read_in_full_if_content_length
+    a, b = 300, 3
+    init_parser('.' * b, 300)
+    assert_equal 300, @parser.content_length
+    ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf)
+    pid = fork {
+      @wr.write('.' * 197)
+      sleep 1 # still a *potential* race here that would make the test moot...
+      @wr.write('.' * 100)
+    }
+    assert_equal a, ti.read(a).size
+    _, status = Process.waitpid2(pid)
+    assert status.success?
+    @wr.close
+  end
+
+  def test_big_body_multi
+    init_parser('.', Unicorn::Const::MAX_BODY + 1)
+    ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf)
+    assert_equal Unicorn::Const::MAX_BODY, @parser.content_length
+    assert ! @parser.body_eof?
+    assert_kind_of File, ti.instance_eval { @tmp }
+    assert_equal 0, ti.instance_eval { @tmp.pos }
+    assert_equal 1, ti.instance_eval { @tmp.size }
+    assert_equal Unicorn::Const::MAX_BODY + 1, ti.size
+    nr = Unicorn::Const::MAX_BODY / 4
+    pid = fork {
+      @rd.close
+      nr.times { @wr.write('....') }
+      @wr.close
+    }
+    @wr.close
+    assert_equal '.', ti.read(1)
+    assert_equal Unicorn::Const::MAX_BODY + 1, ti.size
+    nr.times {
+      assert_equal '....', ti.read(4)
+      assert_equal Unicorn::Const::MAX_BODY + 1, ti.size
+    }
+    assert_nil ti.read(1)
+    status = nil
+    assert_nothing_raised { pid, status = Process.waitpid2(pid) }
+    assert status.success?
+  end
+
+  def test_chunked
+    @parser = Unicorn::HttpParser.new
+    @buf = "POST / HTTP/1.1\r\n" \
+           "Host: localhost\r\n" \
+           "Transfer-Encoding: chunked\r\n" \
+           "\r\n"
+    assert_equal @env, @parser.headers(@env, @buf)
+    assert_equal "", @buf
+
+    pid = fork {
+      @rd.close
+      5.times { @wr.write("5\r\nabcde\r\n") }
+      @wr.write("0\r\n")
+    }
+    @wr.close
+    ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf)
+    assert_nil @parser.content_length
+    assert_nil ti.instance_eval { @size }
+    assert ! @parser.body_eof?
+    assert_equal 25, ti.size
+    assert @parser.body_eof?
+    assert_equal 25, ti.instance_eval { @size }
+    assert_equal 0, ti.instance_eval { @tmp.pos }
+    assert_nothing_raised { ti.rewind }
+    assert_equal 0, ti.instance_eval { @tmp.pos }
+    assert_equal 'abcdeabcdeabcdeabcde', ti.read(20)
+    assert_equal 20, ti.instance_eval { @tmp.pos }
+    assert_nothing_raised { ti.rewind }
+    assert_equal 0, ti.instance_eval { @tmp.pos }
+    assert_kind_of File, ti.instance_eval { @tmp }
+    status = nil
+    assert_nothing_raised { pid, status = Process.waitpid2(pid) }
+    assert status.success?
+  end
+
+  def test_chunked_ping_pong
+    @parser = Unicorn::HttpParser.new
+    @buf = "POST / HTTP/1.1\r\n" \
+           "Host: localhost\r\n" \
+           "Transfer-Encoding: chunked\r\n" \
+           "\r\n"
+    assert_equal @env, @parser.headers(@env, @buf)
+    assert_equal "", @buf
+    chunks = %w(aa bbb cccc dddd eeee)
+    rd, wr = IO.pipe
+
+    pid = fork {
+      chunks.each do |chunk|
+        rd.read(1) == "." and
+          @wr.write("#{'%x' % [ chunk.size]}\r\n#{chunk}\r\n")
+      end
+      @wr.write("0\r\n")
+    }
+    ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf)
+    assert_nil @parser.content_length
+    assert_nil ti.instance_eval { @size }
+    assert ! @parser.body_eof?
+    chunks.each do |chunk|
+      wr.write('.')
+      assert_equal chunk, ti.read(16384)
+    end
+    _, status = Process.waitpid2(pid)
+    assert status.success?
+  end
+
+private
+
+  def init_parser(body, size = nil)
+    @parser = Unicorn::HttpParser.new
+    body = body.to_s.freeze
+    @buf = "POST / HTTP/1.1\r\n" \
+           "Host: localhost\r\n" \
+           "Content-Length: #{size || body.size}\r\n" \
+           "\r\n#{body}"
+    assert_equal @env, @parser.headers(@env, @buf)
+    assert_equal body, @buf
+  end
+
+end
diff --git a/test/unit/test_upload.rb b/test/unit/test_upload.rb
index 9ef3ed7..7ac3c9e 100644
--- a/test/unit/test_upload.rb
+++ b/test/unit/test_upload.rb
@@ -1,5 +1,8 @@
+# -*- encoding: binary -*-
+
 # Copyright (c) 2009 Eric Wong
 require 'test/test_helper'
+require 'digest/md5'
 
 include Unicorn
 
@@ -18,29 +21,33 @@ class UploadTest < Test::Unit::TestCase
     @sha1 = Digest::SHA1.new
     @sha1_app = lambda do |env|
       input = env['rack.input']
-      resp = { :pos => input.pos, :size => input.size, :class => input.class }
+      resp = {}
 
-      # sysread
       @sha1.reset
-      begin
-        loop { @sha1.update(input.sysread(@bs)) }
-      rescue EOFError
+      while buf = input.read(@bs)
+        @sha1.update(buf)
       end
       resp[:sha1] = @sha1.hexdigest
 
-      # read
-      input.sysseek(0) if input.respond_to?(:sysseek)
+      # rewind and read again
       input.rewind
       @sha1.reset
-      loop {
-        buf = input.read(@bs) or break
+      while buf = input.read(@bs)
         @sha1.update(buf)
-      }
+      end
 
       if resp[:sha1] == @sha1.hexdigest
         resp[:sysread_read_byte_match] = true
       end
 
+      if expect_size = env['HTTP_X_EXPECT_SIZE']
+        if expect_size.to_i == input.size
+          resp[:expect_size_match] = true
+        end
+      end
+      resp[:size] = input.size
+      resp[:content_md5] = env['HTTP_CONTENT_MD5']
+
       [ 200, @hdr.merge({'X-Resp' => resp.inspect}), [] ]
     end
   end
@@ -54,7 +61,7 @@ class UploadTest < Test::Unit::TestCase
     start_server(@sha1_app)
     sock = TCPSocket.new(@addr, @port)
     sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
-    @count.times do
+    @count.times do |i|
       buf = @random.sysread(@bs)
       @sha1.update(buf)
       sock.syswrite(buf)
@@ -63,10 +70,34 @@ class UploadTest < Test::Unit::TestCase
     assert_equal "HTTP/1.1 200 OK", read[0]
     resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
     assert_equal length, resp[:size]
-    assert_equal 0, resp[:pos]
     assert_equal @sha1.hexdigest, resp[:sha1]
   end
 
+  def test_put_content_md5
+    md5 = Digest::MD5.new
+    start_server(@sha1_app)
+    sock = TCPSocket.new(@addr, @port)
+    sock.syswrite("PUT / HTTP/1.0\r\nTransfer-Encoding: chunked\r\n" \
+                  "Trailer: Content-MD5\r\n\r\n")
+    @count.times do |i|
+      buf = @random.sysread(@bs)
+      @sha1.update(buf)
+      md5.update(buf)
+      sock.syswrite("#{'%x' % buf.size}\r\n")
+      sock.syswrite(buf << "\r\n")
+    end
+    sock.syswrite("0\r\n")
+
+    content_md5 = [ md5.digest! ].pack('m').strip.freeze
+    sock.syswrite("Content-MD5: #{content_md5}\r\n\r\n")
+    read = sock.read.split(/\r\n/)
+    assert_equal "HTTP/1.1 200 OK", read[0]
+    resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
+    assert_equal length, resp[:size]
+    assert_equal @sha1.hexdigest, resp[:sha1]
+    assert_equal content_md5, resp[:content_md5]
+  end
+
   def test_put_trickle_small
     @count, @bs = 2, 128
     start_server(@sha1_app)
@@ -85,42 +116,7 @@ class UploadTest < Test::Unit::TestCase
     assert_equal "HTTP/1.1 200 OK", read[0]
     resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
     assert_equal length, resp[:size]
-    assert_equal 0, resp[:pos]
     assert_equal @sha1.hexdigest, resp[:sha1]
-    assert_equal StringIO, resp[:class]
-  end
-
-  def test_tempfile_unlinked
-    spew_path = lambda do |env|
-      if orig = env['HTTP_X_OLD_PATH']
-        assert orig != env['rack.input'].path
-      end
-      assert_equal length, env['rack.input'].size
-      [ 200, @hdr.merge('X-Tempfile-Path' => env['rack.input'].path), [] ]
-    end
-    start_server(spew_path)
-    sock = TCPSocket.new(@addr, @port)
-    sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
-    @count.times { sock.syswrite(' ' * @bs) }
-    path = sock.read[/^X-Tempfile-Path: (\S+)/, 1]
-    sock.close
-
-    # send another request to ensure we hit the next request
-    sock = TCPSocket.new(@addr, @port)
-    sock.syswrite("PUT / HTTP/1.0\r\nX-Old-Path: #{path}\r\n" \
-                  "Content-Length: #{length}\r\n\r\n")
-    @count.times { sock.syswrite(' ' * @bs) }
-    path2 = sock.read[/^X-Tempfile-Path: (\S+)/, 1]
-    sock.close
-    assert path != path2
-
-    # make sure the next request comes in so the unlink got processed
-    sock = TCPSocket.new(@addr, @port)
-    sock.syswrite("GET ?lasdf\r\n\r\n\r\n\r\n")
-    sock.sysread(4096) rescue nil
-    sock.close
-
-    assert ! File.exist?(path)
   end
 
   def test_put_keepalive_truncates_small_overwrite
@@ -136,75 +132,31 @@ class UploadTest < Test::Unit::TestCase
     sock.syswrite('12345') # write 4 bytes more than we expected
     @sha1.update('1')
 
-    read = sock.read.split(/\r\n/)
+    buf = sock.readpartial(4096)
+    while buf !~ /\r\n\r\n/
+      buf << sock.readpartial(4096)
+    end
+    read = buf.split(/\r\n/)
     assert_equal "HTTP/1.1 200 OK", read[0]
     resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
     assert_equal to_upload, resp[:size]
-    assert_equal 0, resp[:pos]
     assert_equal @sha1.hexdigest, resp[:sha1]
   end
 
   def test_put_excessive_overwrite_closed
-    start_server(lambda { |env| [ 200, @hdr, [] ] })
-    sock = TCPSocket.new(@addr, @port)
-    buf = ' ' * @bs
-    sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
-    @count.times { sock.syswrite(buf) }
-    assert_raise(Errno::ECONNRESET, Errno::EPIPE) do
-      ::Unicorn::Const::CHUNK_SIZE.times { sock.syswrite(buf) }
-    end
-  end
-
-  def test_put_handler_closed_file
-    nr = '0'
     start_server(lambda { |env|
-      env['rack.input'].close
-      resp = { :nr => nr.succ! }
-      [ 200, @hdr.merge({ 'X-Resp' => resp.inspect}), [] ]
+      while env['rack.input'].read(65536); end
+      [ 200, @hdr, [] ]
     })
     sock = TCPSocket.new(@addr, @port)
     buf = ' ' * @bs
     sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
-    @count.times { sock.syswrite(buf) }
-    read = sock.read.split(/\r\n/)
-    assert_equal "HTTP/1.1 200 OK", read[0]
-    resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
-    assert_equal '1', resp[:nr]
 
-    # server still alive?
-    sock = TCPSocket.new(@addr, @port)
-    sock.syswrite("GET / HTTP/1.0\r\n\r\n")
-    read = sock.read.split(/\r\n/)
-    assert_equal "HTTP/1.1 200 OK", read[0]
-    resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
-    assert_equal '2', resp[:nr]
-  end
-
-  def test_renamed_file_not_closed
-    start_server(lambda { |env|
-      new_tmp = Tempfile.new('unicorn_test')
-      input = env['rack.input']
-      File.rename(input.path, new_tmp.path)
-      resp = {
-        :inode => input.stat.ino,
-        :size => input.stat.size,
-        :new_tmp => new_tmp.path,
-        :old_tmp => input.path,
-      }
-      [ 200, @hdr.merge({ 'X-Resp' => resp.inspect}), [] ]
-    })
-    sock = TCPSocket.new(@addr, @port)
-    buf = ' ' * @bs
-    sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
     @count.times { sock.syswrite(buf) }
-    read = sock.read.split(/\r\n/)
-    assert_equal "HTTP/1.1 200 OK", read[0]
-    resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
-    new_tmp = File.open(resp[:new_tmp])
-    assert_equal resp[:inode], new_tmp.stat.ino
-    assert_equal length, resp[:size]
-    assert ! File.exist?(resp[:old_tmp])
-    assert_equal resp[:size], new_tmp.stat.size
+    assert_raise(Errno::ECONNRESET, Errno::EPIPE) do
+      ::Unicorn::Const::CHUNK_SIZE.times { sock.syswrite(buf) }
+    end
+    assert_equal "HTTP/1.1 200 OK\r\n", sock.gets
   end
 
   # Despite reading numerous articles and inspecting the 1.9.1-p0 C
@@ -233,7 +185,6 @@ class UploadTest < Test::Unit::TestCase
     resp = `curl -isSfN -T#{tmp.path} http://#@addr:#@port/`
     assert $?.success?, 'curl ran OK'
     assert_match(%r!\b#{sha1}\b!, resp)
-    assert_match(/Tempfile/, resp)
     assert_match(/sysread_read_byte_match/, resp)
 
     # small StringIO path
@@ -249,10 +200,87 @@ class UploadTest < Test::Unit::TestCase
     resp = `curl -isSfN -T#{tmp.path} http://#@addr:#@port/`
     assert $?.success?, 'curl ran OK'
     assert_match(%r!\b#{sha1}\b!, resp)
-    assert_match(/StringIO/, resp)
     assert_match(/sysread_read_byte_match/, resp)
   end
 
+  def test_chunked_upload_via_curl
+    # POSIX doesn't require all of these to be present on a system
+    which('curl') or return
+    which('sha1sum') or return
+    which('dd') or return
+
+    start_server(@sha1_app)
+
+    tmp = Tempfile.new('dd_dest')
+    assert(system("dd", "if=#{@random.path}", "of=#{tmp.path}",
+                        "bs=#{@bs}", "count=#{@count}"),
+           "dd #@random to #{tmp}")
+    sha1_re = %r!\b([a-f0-9]{40})\b!
+    sha1_out = `sha1sum #{tmp.path}`
+    assert $?.success?, 'sha1sum ran OK'
+
+    assert_match(sha1_re, sha1_out)
+    sha1 = sha1_re.match(sha1_out)[1]
+    cmd = "curl -H 'X-Expect-Size: #{tmp.size}' --tcp-nodelay \
+           -isSf --no-buffer -T- " \
+          "http://#@addr:#@port/"
+    resp = Tempfile.new('resp')
+    resp.sync = true
+
+    rd, wr = IO.pipe
+    wr.sync = rd.sync = true
+    pid = fork {
+      STDIN.reopen(rd)
+      rd.close
+      wr.close
+      STDOUT.reopen(resp)
+      exec cmd
+    }
+    rd.close
+
+    tmp.rewind
+    @count.times { |i|
+      wr.write(tmp.read(@bs))
+      sleep(rand / 10) if 0 == i % 8
+    }
+    wr.close
+    pid, status = Process.waitpid2(pid)
+
+    resp.rewind
+    resp = resp.read
+    assert status.success?, 'curl ran OK'
+    assert_match(%r!\b#{sha1}\b!, resp)
+    assert_match(/sysread_read_byte_match/, resp)
+    assert_match(/expect_size_match/, resp)
+  end
+
+  def test_curl_chunked_small
+    # POSIX doesn't require all of these to be present on a system
+    which('curl') or return
+    which('sha1sum') or return
+    which('dd') or return
+
+    start_server(@sha1_app)
+
+    tmp = Tempfile.new('dd_dest')
+    # small StringIO path
+    assert(system("dd", "if=#{@random.path}", "of=#{tmp.path}",
+                        "bs=1024", "count=1"),
+           "dd #@random to #{tmp}")
+    sha1_re = %r!\b([a-f0-9]{40})\b!
+    sha1_out = `sha1sum #{tmp.path}`
+    assert $?.success?, 'sha1sum ran OK'
+
+    assert_match(sha1_re, sha1_out)
+    sha1 = sha1_re.match(sha1_out)[1]
+    resp = `curl -H 'X-Expect-Size: #{tmp.size}' --tcp-nodelay \
+            -isSf --no-buffer -T- http://#@addr:#@port/ < #{tmp.path}`
+    assert $?.success?, 'curl ran OK'
+    assert_match(%r!\b#{sha1}\b!, resp)
+    assert_match(/sysread_read_byte_match/, resp)
+    assert_match(/expect_size_match/, resp)
+  end
+
   private
 
   def length
diff --git a/test/unit/test_util.rb b/test/unit/test_util.rb
index 032f0be..4a1e21f 100644
--- a/test/unit/test_util.rb
+++ b/test/unit/test_util.rb
@@ -1,3 +1,5 @@
+# -*- encoding: binary -*-
+
 require 'test/test_helper'
 require 'tempfile'
 
@@ -15,6 +17,7 @@ class TestUtil < Test::Unit::TestCase
     assert_equal before, File.stat(tmp.path).inspect
     assert_equal ext, (tmp.external_encoding rescue nil)
     assert_equal int, (tmp.internal_encoding rescue nil)
+    assert_nothing_raised { tmp.close! }
   end
 
   def test_reopen_logs_renamed
@@ -37,6 +40,8 @@ class TestUtil < Test::Unit::TestCase
     assert_equal int, (tmp.internal_encoding rescue nil)
     assert_equal(EXPECT_FLAGS, EXPECT_FLAGS & tmp.fcntl(Fcntl::F_GETFL))
     assert tmp.sync
+    assert_nothing_raised { tmp.close! }
+    assert_nothing_raised { to.close! }
   end
 
   def test_reopen_logs_renamed_with_encoding
@@ -59,6 +64,7 @@ class TestUtil < Test::Unit::TestCase
         assert fp.sync
       }
     }
+    assert_nothing_raised { tmp.close! }
   end if STDIN.respond_to?(:external_encoding)
 
   def test_reopen_logs_renamed_with_internal_encoding
@@ -84,6 +90,7 @@ class TestUtil < Test::Unit::TestCase
         }
       }
     }
+    assert_nothing_raised { tmp.close! }
   end if STDIN.respond_to?(:external_encoding)
 
 end