summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTres Seaver <tseaver@palladion.com>2014-03-10 11:24:39 -0400
committerTres Seaver <tseaver@palladion.com>2014-03-10 11:24:39 -0400
commit78417e17ee193befba1b1bc2c5b4b1a1629d8320 (patch)
treebe95173fd29f90a59e8cfe36fe47bd1ae0951182
parent7b2ff7fed602a4dc00d1a64e617deb7a1ce529bb (diff)
parent169558586d477f6f22402300422b90b5334b3654 (diff)
downloadwaitress-78417e17ee193befba1b1bc2c5b4b1a1629d8320.tar.gz
Merge from master.
-rw-r--r--CHANGES.txt51
-rw-r--r--docs/conf.py2
-rw-r--r--docs/runner.rst2
-rw-r--r--setup.py2
-rw-r--r--waitress/adjustments.py24
-rw-r--r--waitress/buffers.py16
-rw-r--r--waitress/channel.py9
-rw-r--r--waitress/compat.py5
-rw-r--r--waitress/parser.py17
-rw-r--r--waitress/receiver.py6
-rw-r--r--waitress/task.py39
-rw-r--r--waitress/tests/test_adjustments.py10
-rw-r--r--waitress/tests/test_buffers.py35
-rw-r--r--waitress/tests/test_channel.py32
-rw-r--r--waitress/tests/test_functional.py4
-rw-r--r--waitress/tests/test_parser.py24
-rw-r--r--waitress/tests/test_receiver.py23
-rw-r--r--waitress/tests/test_runner.py4
-rw-r--r--waitress/tests/test_task.py22
19 files changed, 283 insertions, 44 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 3317251..1722bf9 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -3,7 +3,56 @@ Unreleased
- Allow override of ``wsgi.url_scheme`` via a request header,
``X_FORWARDED_PROTO``. Allows proxies which serve mixed HTTP / HTTPS
- requests to control signal which are served as HTTPS.
+ requests to control signal which are served as HTTPS.See
+ https://github.com/Pylons/waitress/pull/42.
+
+0.8.8 (2013-11-30)
+------------------
+
+- Fix some cases where the creation of extremely large output buffers (greater
+ than 2GB, suspected to be buffers added via ``wsgi.file_wrapper``) might
+ cause an OverflowError on Python 2. See
+ https://github.com/Pylons/waitress/issues/47.
+
+- When the ``url_prefix`` adjustment starts with more than one slash, all
+ slashes except one will be stripped from its beginning. This differs from
+ older behavior where more than one leading slash would be preserved in
+ ``url_prefix``.
+
+- If a client somehow manages to send an empty path, we no longer convert the
+ empty path to a single slash in ``PATH_INFO``. Instead, the path remains
+ empty. According to RFC 2616 section "5.1.2 Request-URI", the scenario of a
+ client sending an empty path is actually not possible because the request URI
+ portion cannot be empty.
+
+- If the ``url_prefix`` adjustment matches the request path exactly, we now
+ compute ``SCRIPT_NAME`` and ``PATH_INFO`` properly. Previously, if the
+ ``url_prefix`` was ``/foo`` and the path received from a client was ``/foo``,
+ we would set *both* ``SCRIPT_NAME`` and ``PATH_INFO`` to ``/foo``. This was
+ incorrect. Now in such a case we set ``PATH_INFO`` to the empty string and
+ we set ``SCRIPT_NAME`` to ``/foo``. Note that the change we made has no
+ effect on paths that do not match the ``url_prefix`` exactly (such as
+ ``/foo/bar``); these continue to operate as they did. See
+ https://github.com/Pylons/waitress/issues/46
+
+- Preserve header ordering of headers with the same name as per RFC 2616. See
+ https://github.com/Pylons/waitress/pull/44
+
+- When waitress receives a ``Transfer-Encoding: chunked`` request, we no longer
+ send the ``TRANSFER_ENCODING`` nor the ``HTTP_TRANSFER_ENCODING`` value to
+ the application in the environment. Instead, we pop this header. Since we
+ cope with chunked requests by buffering the data in the server, we also know
+ when a chunked request has ended, and therefore we know the content length.
+ We set the content-length header in the environment, such that applications
+ effectively never know the original request was a T-E: chunked request; it
+ will appear to them as if the request is a non-chunked request with an
+ accurate content-length.
+
+- Cope with the fact that the ``Transfer-Encoding`` value is case-insensitive.
+
+- When the ``--unix-socket-perms`` option was used as an argument to
+ ``waitress-serve``, a ``TypeError`` would be raised. See
+ https://github.com/Pylons/waitress/issues/50.
0.8.7 (2013-08-29)
------------------
diff --git a/docs/conf.py b/docs/conf.py
index fa9f9a7..1df17f3 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -65,7 +65,7 @@ copyright = '2012, Agendaless Consulting <chrism@plope.com>'
# other places throughout the built documents.
#
# The short X.Y version.
-version = '0.8.7'
+version = '0.8.8'
# The full version, including alpha/beta/rc tags.
release = version
diff --git a/docs/runner.rst b/docs/runner.rst
index 3fb3814..9799a17 100644
--- a/docs/runner.rst
+++ b/docs/runner.rst
@@ -121,7 +121,7 @@ Tuning options:
Number of bytes to request when calling ``socket.recv()``. Default is
8192.
-``--send-bytes=INT```
+``--send-bytes=INT``
Number of bytes to send to socket.send(). Default is 18000.
Multiples of 9000 should avoid partly-filled TCP packets.
diff --git a/setup.py b/setup.py
index f28271a..4d5307d 100644
--- a/setup.py
+++ b/setup.py
@@ -37,7 +37,7 @@ if sys.version_info[:2] == (2, 6):
setup(
name='waitress',
- version='0.8.7',
+ version='0.8.8',
author='Zope Foundation and Contributors',
author_email='zope-dev@zope.org',
maintainer="Chris McDonough",
diff --git a/waitress/adjustments.py b/waitress/adjustments.py
index fb5f7a6..2835e97 100644
--- a/waitress/adjustments.py
+++ b/waitress/adjustments.py
@@ -36,8 +36,13 @@ def asoctal(s):
"""Convert the given octal string to an actual number."""
return int(s, 8)
-def slash_suffix_stripped_str(s):
- return s.rstrip('/')
+def slash_fixed_str(s):
+ s = s.strip()
+ if s:
+ # always have a leading slash, replace any number of leading slashes
+ # with a single slash, and strip any trailing slashes
+ s = '/' + s.lstrip('/').rstrip('/')
+ return s
class Adjustments(object):
"""This class contains tunable parameters.
@@ -48,7 +53,7 @@ class Adjustments(object):
('port', int),
('threads', int),
('url_scheme', str),
- ('url_prefix', slash_suffix_stripped_str),
+ ('url_prefix', slash_fixed_str),
('backlog', int),
('recv_bytes', int),
('send_bytes', int),
@@ -176,7 +181,10 @@ class Adjustments(object):
@classmethod
def parse_args(cls, argv):
- """Parse command line arguments.
+ """Pre-parse command line arguments for input into __init__. Note that
+ this does not cast values into adjustment types, it just creates a
+ dictionary suitable for passing into __init__, where __init__ does the
+ casting.
"""
long_opts = ['help', 'call']
for opt, cast in cls._params:
@@ -196,9 +204,11 @@ class Adjustments(object):
param = opt.lstrip('-').replace('-', '_')
if param.startswith('no_'):
param = param[3:]
- kw[param] = False
- elif param in ('help', 'call') or cls._param_map[param] is asbool:
+ kw[param] = 'false'
+ elif param in ('help', 'call'):
kw[param] = True
+ elif cls._param_map[param] is asbool:
+ kw[param] = 'true'
else:
- kw[param] = cls._param_map[param](value)
+ kw[param] = value
return kw, args
diff --git a/waitress/buffers.py b/waitress/buffers.py
index bb5a530..0009444 100644
--- a/waitress/buffers.py
+++ b/waitress/buffers.py
@@ -186,8 +186,8 @@ class OverflowableBuffer(object):
"""
This buffer implementation has four stages:
- No data
- - String-based buffer
- - StringIO-based buffer
+ - Bytes-based buffer
+ - BytesIO-based buffer
- Temporary file storage
The first two stages are fastest for simple transfers.
"""
@@ -203,11 +203,15 @@ class OverflowableBuffer(object):
def __len__(self):
buf = self.buf
if buf is not None:
+ # use buf.__len__ rather than len(buf) FBO of not getting
+ # OverflowError on Python 2
return buf.__len__()
else:
return self.strbuf.__len__()
def __nonzero__(self):
+ # use self.__len__ rather than len(self) FBO of not getting
+ # OverflowError on Python 2
return self.__len__() > 0
__bool__ = __nonzero__ # py3
@@ -241,7 +245,9 @@ class OverflowableBuffer(object):
return
buf = self._create_buffer()
buf.append(s)
- sz = len(buf)
+ # use buf.__len__ rather than len(buf) FBO of not getting
+ # OverflowError on Python 2
+ sz = buf.__len__()
if not self.overflowed:
if sz >= self.overflow:
self._set_large_buffer()
@@ -278,7 +284,9 @@ class OverflowableBuffer(object):
return
buf.prune()
if self.overflowed:
- sz = len(buf)
+ # use buf.__len__ rather than len(buf) FBO of not getting
+ # OverflowError on Python 2
+ sz = buf.__len__()
if sz < self.overflow:
# Revert to a faster buffer.
self._set_small_buffer()
diff --git a/waitress/channel.py b/waitress/channel.py
index 5d16c74..11ac140 100644
--- a/waitress/channel.py
+++ b/waitress/channel.py
@@ -89,7 +89,10 @@ class HTTPChannel(logging_dispatcher, object):
return False
def total_outbufs_len(self):
- return sum([len(b) for b in self.outbufs]) # genexpr == more funccalls
+ # genexpr == more funccalls
+ # use b.__len__ rather than len(b) FBO of not getting OverflowError
+ # on Python 2
+ return sum([b.__len__() for b in self.outbufs])
def writable(self):
# if there's data in the out buffer or we've been instructed to close
@@ -233,7 +236,9 @@ class HTTPChannel(logging_dispatcher, object):
while True:
outbuf = self.outbufs[0]
- outbuflen = len(outbuf)
+ # use outbuf.__len__ rather than len(outbuf) FBO of not getting
+ # OverflowError on Python 2
+ outbuflen = outbuf.__len__()
if outbuflen <= 0:
# self.outbufs[-1] must always be a writable outbuf
if len(self.outbufs) > 1:
diff --git a/waitress/compat.py b/waitress/compat.py
index 1796480..a90ee56 100644
--- a/waitress/compat.py
+++ b/waitress/compat.py
@@ -109,3 +109,8 @@ try:
import httplib
except ImportError: # pragma: no cover
from http import client as httplib
+
+try:
+ MAXINT = sys.maxint
+except AttributeError: # pragma: no cover
+ MAXINT = sys.maxsize
diff --git a/waitress/parser.py b/waitress/parser.py
index 83e2424..dc3ec47 100644
--- a/waitress/parser.py
+++ b/waitress/parser.py
@@ -148,7 +148,16 @@ class HTTPRequestParser(object):
self.error = br.error
self.completed = True
elif br.completed:
+ # The request (with the body) is ready to use.
self.completed = True
+ if self.chunked:
+ # We've converted the chunked transfer encoding request
+ # body into a normal request body, so we know its content
+ # length; set the header here. We already popped the
+ # TRANSFER_ENCODING header in parse_header, so this will
+ # appear to the client to be an entirely non-chunked HTTP
+ # request with a valid content-length.
+ self.headers['CONTENT_LENGTH'] = str(br.__len__())
return consumed
def parse_header(self, header_plus):
@@ -203,8 +212,12 @@ class HTTPRequestParser(object):
self.connection_close = True
if version == '1.1':
- te = headers.get('TRANSFER_ENCODING', '')
- if te == 'chunked':
+ # since the server buffers data from chunked transfers and clients
+ # never need to deal with chunked requests, downstream clients
+ # should not see the HTTP_TRANSFER_ENCODING header; we pop it
+ # here
+ te = headers.pop('TRANSFER_ENCODING', '')
+ if te.lower() == 'chunked':
self.chunked = True
buf = OverflowableBuffer(self.adj.inbuf_overflow)
self.body_rcv = ChunkedReceiver(buf)
diff --git a/waitress/receiver.py b/waitress/receiver.py
index f362f08..594ae97 100644
--- a/waitress/receiver.py
+++ b/waitress/receiver.py
@@ -28,6 +28,9 @@ class FixedStreamReceiver(object):
self.remain = cl
self.buf = buf
+ def __len__(self):
+ return self.buf.__len__()
+
def received(self, data):
'See IStreamConsumer'
rm = self.remain
@@ -66,6 +69,9 @@ class ChunkedReceiver(object):
def __init__(self, buf):
self.buf = buf
+ def __len__(self):
+ return self.buf.__len__()
+
def received(self, s):
# Returns the number of bytes consumed.
if self.completed:
diff --git a/waitress/task.py b/waitress/task.py
index c541dfd..7dcd8a4 100644
--- a/waitress/task.py
+++ b/waitress/task.py
@@ -255,7 +255,11 @@ class Task(object):
response_headers.append(('Date', build_http_date(self.start_time)))
first_line = 'HTTP/%s %s' % (self.version, self.status)
- next_lines = ['%s: %s' % hv for hv in sorted(self.response_headers)]
+ # NB: sorting headers needs to preserve same-named-header order
+ # as per RFC 2616 section 4.2; thus the key=lambda x: x[0] here;
+ # rely on stable sort to keep relative position of same-named headers
+ next_lines = ['%s: %s' % hv for hv in sorted(
+ self.response_headers, key=lambda x: x[0])]
lines = [first_line] + next_lines
res = '%s\r\n\r\n' % '\r\n'.join(lines)
return tobytes(res)
@@ -447,13 +451,28 @@ class WSGITask(Task):
path = request.path
channel = self.channel
server = channel.server
-
- path = path.lstrip('/')
-
- url_prefix_with_slash = server.adj.url_prefix.lstrip('/') + '/'
-
- if url_prefix_with_slash and path.startswith(url_prefix_with_slash):
- path = path[len(url_prefix_with_slash):]
+ url_prefix = server.adj.url_prefix
+
+ if path.startswith('/'):
+ # strip extra slashes at the beginning of a path that starts
+ # with any number of slashes
+ path = '/' + path.lstrip('/')
+
+ if url_prefix:
+ # NB: url_prefix is guaranteed by the configuration machinery to
+ # be either the empty string or a string that starts with a single
+ # slash and ends without any slashes
+ if path == url_prefix:
+ # if the path is the same as the url prefix, the SCRIPT_NAME
+ # should be the url_prefix and PATH_INFO should be empty
+ path = ''
+ else:
+ # if the path starts with the url prefix plus a slash,
+ # the SCRIPT_NAME should be the url_prefix and PATH_INFO should
+ # the value of path from the slash until its end
+ url_prefix_with_trailing_slash = url_prefix + '/'
+ if path.startswith(url_prefix_with_trailing_slash):
+ path = path[len(url_prefix):]
environ = {}
environ['REQUEST_METHOD'] = request.command.upper()
@@ -461,8 +480,8 @@ class WSGITask(Task):
environ['SERVER_NAME'] = server.server_name
environ['SERVER_SOFTWARE'] = server.adj.ident
environ['SERVER_PROTOCOL'] = 'HTTP/%s' % self.version
- environ['SCRIPT_NAME'] = server.adj.url_prefix
- environ['PATH_INFO'] = '/' + path
+ environ['SCRIPT_NAME'] = url_prefix
+ environ['PATH_INFO'] = path
environ['QUERY_STRING'] = request.query
environ['REMOTE_ADDR'] = channel.addr[0]
diff --git a/waitress/tests/test_adjustments.py b/waitress/tests/test_adjustments.py
index 26d7b04..fe390dc 100644
--- a/waitress/tests/test_adjustments.py
+++ b/waitress/tests/test_adjustments.py
@@ -59,7 +59,7 @@ class TestAdjustments(unittest.TestCase):
max_request_header_size='1300', max_request_body_size='1400',
expose_tracebacks='true', ident='abc', asyncore_loop_timeout='5',
asyncore_use_poll=True, unix_socket='/tmp/waitress.sock',
- unix_socket_perms='777', url_prefix='/foo')
+ unix_socket_perms='777', url_prefix='///foo/')
self.assertEqual(inst.host, 'host')
self.assertEqual(inst.port, 8080)
self.assertEqual(inst.threads, 5)
@@ -114,12 +114,12 @@ class TestCLI(unittest.TestCase):
def test_positive_boolean(self):
opts, args = self.parse(['--expose-tracebacks'])
- self.assertDictContainsSubset({'expose_tracebacks': True}, opts)
+ self.assertDictContainsSubset({'expose_tracebacks': 'true'}, opts)
self.assertSequenceEqual(args, [])
def test_negative_boolean(self):
opts, args = self.parse(['--no-expose-tracebacks'])
- self.assertDictContainsSubset({'expose_tracebacks': False}, opts)
+ self.assertDictContainsSubset({'expose_tracebacks': 'false'}, opts)
self.assertSequenceEqual(args, [])
def test_cast_params(self):
@@ -130,8 +130,8 @@ class TestCLI(unittest.TestCase):
])
self.assertDictContainsSubset({
'host': 'localhost',
- 'port': 80,
- 'unix_socket_perms': 0o777,
+ 'port': '80',
+ 'unix_socket_perms':'777',
}, opts)
self.assertSequenceEqual(args, [])
diff --git a/waitress/tests/test_buffers.py b/waitress/tests/test_buffers.py
index b4ed7eb..f7c90b4 100644
--- a/waitress/tests/test_buffers.py
+++ b/waitress/tests/test_buffers.py
@@ -284,6 +284,17 @@ class TestOverflowableBuffer(unittest.TestCase):
self.assertEqual(inst.buf.get(100), b'x' * 5)
self.assertEqual(inst.strbuf, b'')
+ def test_append_with_len_more_than_max_int(self):
+ from waitress.compat import MAXINT
+ inst = self._makeOne()
+ inst.overflowed = True
+ buf = DummyBuffer(length=MAXINT)
+ inst.buf = buf
+ result = inst.append(b'x')
+ # we don't want this to throw an OverflowError on Python 2 (see
+ # https://github.com/Pylons/waitress/issues/47)
+ self.assertEqual(result, None)
+
def test_append_buf_None_not_longer_than_srtbuf_limit(self):
inst = self._makeOne()
inst.strbuf = b'x' * 5
@@ -373,6 +384,17 @@ class TestOverflowableBuffer(unittest.TestCase):
inst.prune()
self.assertNotEqual(inst.buf, buf)
+ def test_prune_with_buflen_more_than_max_int(self):
+ from waitress.compat import MAXINT
+ inst = self._makeOne()
+ inst.overflowed = True
+ buf = DummyBuffer(length=MAXINT+1)
+ inst.buf = buf
+ result = inst.prune()
+ # we don't want this to throw an OverflowError on Python 2 (see
+ # https://github.com/Pylons/waitress/issues/47)
+ self.assertEqual(result, None)
+
def test_getfile_buf_None(self):
inst = self._makeOne()
f = inst.getfile()
@@ -417,3 +439,16 @@ class Filelike(KindaFilelike):
def tell(self):
v = self.tellresults.pop(0)
return v
+
+class DummyBuffer(object):
+ def __init__(self, length=0):
+ self.length = length
+
+ def __len__(self):
+ return self.length
+
+ def append(self, s):
+ self.length = self.length + len(s)
+
+ def prune(self):
+ pass
diff --git a/waitress/tests/test_channel.py b/waitress/tests/test_channel.py
index 33286c2..3ca97f0 100644
--- a/waitress/tests/test_channel.py
+++ b/waitress/tests/test_channel.py
@@ -22,6 +22,18 @@ class TestHTTPChannel(unittest.TestCase):
self.assertEqual(inst.addr, '127.0.0.1')
self.assertEqual(map[100], inst)
+ def test_total_outbufs_len_an_outbuf_size_gt_sys_maxint(self):
+ from waitress.compat import MAXINT
+ inst, _, map = self._makeOneWithMap()
+ class DummyHugeBuffer(object):
+ def __len__(self):
+ return MAXINT + 1
+ inst.outbufs = [DummyHugeBuffer()]
+ result = inst.total_outbufs_len()
+ # we are testing that this method does not raise an OverflowError
+ # (see https://github.com/Pylons/waitress/issues/47)
+ self.assertEqual(result, MAXINT+1)
+
def test_writable_something_in_outbuf(self):
inst, sock, map = self._makeOneWithMap()
inst.outbufs[0].append(b'abc')
@@ -256,6 +268,26 @@ class TestHTTPChannel(unittest.TestCase):
self.assertEqual(inst.outbufs, [buffer])
self.assertEqual(len(inst.logger.exceptions), 1)
+ def test__flush_some_outbuf_len_gt_sys_maxint(self):
+ from waitress.compat import MAXINT
+ inst, sock, map = self._makeOneWithMap()
+ class DummyHugeOutbuffer(object):
+ def __init__(self):
+ self.length = MAXINT + 1
+ def __len__(self):
+ return self.length
+ def get(self, numbytes):
+ self.length = 0
+ return b'123'
+ def skip(self, *args): pass
+ buf = DummyHugeOutbuffer()
+ inst.outbufs = [buf]
+ inst.send = lambda *arg: 0
+ result = inst._flush_some()
+ # we are testing that _flush_some doesn't raise an OverflowError
+ # when one of its outbufs has a __len__ that returns gt sys.maxint
+ self.assertEqual(result, False)
+
def test_handle_close(self):
inst, sock, map = self._makeOneWithMap()
inst.handle_close()
diff --git a/waitress/tests/test_functional.py b/waitress/tests/test_functional.py
index afe0929..942ef0a 100644
--- a/waitress/tests/test_functional.py
+++ b/waitress/tests/test_functional.py
@@ -258,6 +258,8 @@ class EchoTests(object):
line, headers, response_body = read_http(fp)
self.assertline(line, '200', 'OK', 'HTTP/1.1')
self.assertEqual(response_body, b'')
+ self.assertEqual(headers['content-length'], '0')
+ self.assertFalse('transfer-encoding' in headers)
def test_chunking_request_with_content(self):
control_line = b"20;\r\n" # 20 hex = 32 dec
@@ -277,6 +279,8 @@ class EchoTests(object):
line, headers, response_body = read_http(fp)
self.assertline(line, '200', 'OK', 'HTTP/1.1')
self.assertEqual(response_body, expected)
+ self.assertEqual(headers['content-length'], str(len(expected)))
+ self.assertFalse('transfer-encoding' in headers)
def test_broken_chunked_encoding(self):
control_line = "20;\r\n" # 20 hex = 32 dec
diff --git a/waitress/tests/test_parser.py b/waitress/tests/test_parser.py
index d51e705..72e3a01 100644
--- a/waitress/tests/test_parser.py
+++ b/waitress/tests/test_parser.py
@@ -121,8 +121,8 @@ GET /foobar HTTP/1.1
Transfer-Encoding: chunked
X-Foo: 1
-20;\r
-This string has 32 characters\r
+20;\r\n
+This string has 32 characters\r\n
0\r\n\r\n"""
result = self.parser.received(data)
self.assertEqual(result, 58)
@@ -149,6 +149,23 @@ garbage
self.assertTrue(isinstance(self.parser.error,
BadRequest))
+ def test_received_chunked_completed_sets_content_length(self):
+ data = b"""\
+GET /foobar HTTP/1.1
+Transfer-Encoding: chunked
+X-Foo: 1
+
+20;\r\n
+This string has 32 characters\r\n
+0\r\n\r\n"""
+ result = self.parser.received(data)
+ self.assertEqual(result, 58)
+ data = data[result:]
+ result = self.parser.received(data)
+ self.assertTrue(self.parser.completed)
+ self.assertTrue(self.parser.error is None)
+ self.assertEqual(self.parser.headers['CONTENT_LENGTH'], '32')
+
def test_parse_header_gardenpath(self):
data = b"""\
GET /foobar HTTP/8.4
@@ -168,7 +185,8 @@ foo: bar"""
self.assertEqual(self.parser.body_rcv, None)
def test_parse_header_11_te_chunked(self):
- data = b"GET /foobar HTTP/1.1\ntransfer-encoding: chunked"
+ # NB: test that capitalization of header value is unimportant
+ data = b"GET /foobar HTTP/1.1\ntransfer-encoding: ChUnKed"
self.parser.parse_header(data)
self.assertEqual(self.parser.body_rcv.__class__.__name__,
'ChunkedReceiver')
diff --git a/waitress/tests/test_receiver.py b/waitress/tests/test_receiver.py
index 961d1c5..707f328 100644
--- a/waitress/tests/test_receiver.py
+++ b/waitress/tests/test_receiver.py
@@ -2,9 +2,9 @@ import unittest
class TestFixedStreamReceiver(unittest.TestCase):
- def _makeOne(self, buf, cl):
+ def _makeOne(self, cl, buf):
from waitress.receiver import FixedStreamReceiver
- return FixedStreamReceiver(buf, cl)
+ return FixedStreamReceiver(cl, buf)
def test_received_remain_lt_1(self):
buf = DummyBuffer()
@@ -42,6 +42,11 @@ class TestFixedStreamReceiver(unittest.TestCase):
inst = self._makeOne(10, buf)
self.assertEqual(inst.getbuf(), buf)
+ def test___len__(self):
+ buf = DummyBuffer(['1', '2'])
+ inst = self._makeOne(10, buf)
+ self.assertEqual(inst.__len__(), 2)
+
class TestChunkedReceiver(unittest.TestCase):
def _makeOne(self, buf):
@@ -142,13 +147,23 @@ class TestChunkedReceiver(unittest.TestCase):
inst = self._makeOne(buf)
self.assertEqual(inst.getbuf(), buf)
+ def test___len__(self):
+ buf = DummyBuffer(['1', '2'])
+ inst = self._makeOne(buf)
+ self.assertEqual(inst.__len__(), 2)
+
class DummyBuffer(object):
- def __init__(self):
- self.data = []
+ def __init__(self, data=None):
+ if data is None:
+ data = []
+ self.data = data
def append(self, s):
self.data.append(s)
def getfile(self):
return self
+
+ def __len__(self):
+ return len(self.data)
diff --git a/waitress/tests/test_runner.py b/waitress/tests/test_runner.py
index 50f6154..23da82e 100644
--- a/waitress/tests/test_runner.py
+++ b/waitress/tests/test_runner.py
@@ -126,7 +126,7 @@ class Test_run(unittest.TestCase):
import waitress.tests.fixtureapps.runner as _apps
def check_server(app, **kw):
self.assertIs(app, _apps.app)
- self.assertDictEqual(kw, {'port': 80})
+ self.assertDictEqual(kw, {'port': '80'})
argv = [
'waitress-serve',
'--port=80',
@@ -138,7 +138,7 @@ class Test_run(unittest.TestCase):
import waitress.tests.fixtureapps.runner as _apps
def check_server(app, **kw):
self.assertIs(app, _apps.app)
- self.assertDictEqual(kw, {'port': 80})
+ self.assertDictEqual(kw, {'port': '80'})
argv = [
'waitress-serve',
'--port=80',
diff --git a/waitress/tests/test_task.py b/waitress/tests/test_task.py
index 19ff471..8abb1e5 100644
--- a/waitress/tests/test_task.py
+++ b/waitress/tests/test_task.py
@@ -394,6 +394,16 @@ class TestWSGITask(unittest.TestCase):
inst.channel.server.application = app
self.assertRaises(AssertionError, inst.execute)
+ def test_preserve_header_value_order(self):
+ def app(environ, start_response):
+ write = start_response('200 OK', [('C', 'b'), ('A', 'b'), ('A', 'a')])
+ write(b'abc')
+ return []
+ inst = self._makeOne()
+ inst.channel.server.application = app
+ inst.execute()
+ self.assertTrue(b'A: b\r\nA: a\r\nC: b\r\n' in inst.channel.written)
+
def test_execute_bad_status_value(self):
def app(environ, start_response):
start_response(None, [])
@@ -548,7 +558,7 @@ class TestWSGITask(unittest.TestCase):
request.path = ''
inst.request = request
environ = inst.get_environment()
- self.assertEqual(environ['PATH_INFO'], '/')
+ self.assertEqual(environ['PATH_INFO'], '')
def test_get_environment_no_query(self):
inst = self._makeOne()
@@ -585,6 +595,16 @@ class TestWSGITask(unittest.TestCase):
self.assertEqual(environ['PATH_INFO'], '/fuz')
self.assertEqual(environ['SCRIPT_NAME'], '/foo')
+ def test_get_environ_with_url_prefix_empty_path(self):
+ inst = self._makeOne()
+ inst.channel.server.adj.url_prefix = '/foo'
+ request = DummyParser()
+ request.path = '/foo'
+ inst.request = request
+ environ = inst.get_environment()
+ self.assertEqual(environ['PATH_INFO'], '')
+ self.assertEqual(environ['SCRIPT_NAME'], '/foo')
+
def test_get_environment_values(self):
import sys
inst = self._makeOne()