New Upstream Release - waitress

Ready changes

Summary

Merged new upstream version: 2.1.2 (was: 2.1.1).

Resulting package

Built on 2022-10-01T10:46 (took 3m19s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases python-waitress-docapt install -t fresh-releases python3-waitress

Lintian Result

Diff

diff --git a/CHANGES.txt b/CHANGES.txt
index eb7093c..17ca87e 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,3 +1,28 @@
+2.1.2
+-----
+
+Bugfix
+~~~~~~
+
+- When expose_tracebacks is enabled waitress would fail to properly encode
+  unicode thereby causing another error during error handling. See
+  https://github.com/Pylons/waitress/pull/378
+
+- Header length checking had a calculation that was done incorrectly when the
+  data was received across multple socket reads. This calculation has been
+  corrected, and no longer will Waitress send back a 413 Request Entity Too
+  Large. See https://github.com/Pylons/waitress/pull/376
+
+Security Bugfix
+~~~~~~~~~~~~~~~
+
+- in 2.1.0 a new feature was introduced that allowed the WSGI thread to start
+  sending data to the socket. However this introduced a race condition whereby
+  a socket may be closed in the sending thread while the main thread is about
+  to call select() therey causing the entire application to be taken down.
+  Waitress will no longer close the socket in the WSGI thread, instead waking
+  up the main thread to cleanup. See https://github.com/Pylons/waitress/pull/377
+
 2.1.1
 -----
 
diff --git a/debian/changelog b/debian/changelog
index f076515..b70f7fc 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+waitress (2.1.2-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Sat, 01 Oct 2022 10:43:35 -0000
+
 waitress (2.1.1-3) unstable; urgency=medium
 
   * Team Upload.
diff --git a/debian/patches/01-fix-sphinxdoc-conf.patch b/debian/patches/01-fix-sphinxdoc-conf.patch
index f0869a9..e2b6dc9 100644
--- a/debian/patches/01-fix-sphinxdoc-conf.patch
+++ b/debian/patches/01-fix-sphinxdoc-conf.patch
@@ -7,10 +7,10 @@ Forwarded: not-needed
  docs/conf.py | 32 +++++++++++++++++---------------
  1 file changed, 17 insertions(+), 15 deletions(-)
 
-diff --git a/docs/conf.py b/docs/conf.py
-index cf0ff9b..6929b78 100644
---- a/docs/conf.py
-+++ b/docs/conf.py
+Index: waitress/docs/conf.py
+===================================================================
+--- waitress.orig/docs/conf.py
++++ waitress/docs/conf.py
 @@ -18,8 +18,8 @@
  # sys.path.append(os.path.abspath('some/directory'))
  
@@ -22,7 +22,7 @@ index cf0ff9b..6929b78 100644
  
  # General configuration
  # ---------------------
-@@ -53,7 +53,9 @@ copyright = "2012-%s, Agendaless Consulting <chrism@plope.com>" % thisyear
+@@ -53,7 +53,9 @@ copyright = "2012-%s, Agendaless Consult
  # other places throughout the built documents.
  #
  # The short X.Y version.
diff --git a/setup.cfg b/setup.cfg
index 69086dc..333766a 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [metadata]
 name = waitress
-version = 2.1.1
+version = 2.1.2
 description = Waitress WSGI server
 long_description = file: README.rst, CHANGES.txt
 long_description_content_type = text/x-rst
diff --git a/src/waitress/adjustments.py b/src/waitress/adjustments.py
index 466b5c4..f2a852c 100644
--- a/src/waitress/adjustments.py
+++ b/src/waitress/adjustments.py
@@ -147,7 +147,7 @@ class Adjustments:
     # TCP port to listen on
     port = _int_marker(8080)
 
-    listen = ["{}:{}".format(host, port)]
+    listen = [f"{host}:{port}"]
 
     # number of threads available for tasks
     threads = 4
@@ -327,7 +327,7 @@ class Adjustments:
         if not isinstance(self.host, _str_marker) or not isinstance(
             self.port, _int_marker
         ):
-            self.listen = ["{}:{}".format(self.host, self.port)]
+            self.listen = [f"{self.host}:{self.port}"]
 
         enabled_families = socket.AF_UNSPEC
 
diff --git a/src/waitress/channel.py b/src/waitress/channel.py
index 948b498..eb59dd3 100644
--- a/src/waitress/channel.py
+++ b/src/waitress/channel.py
@@ -126,10 +126,10 @@ class HTTPChannel(wasyncore.dispatcher):
         if self.will_close:
             self.handle_close()
 
-    def _flush_exception(self, flush):
+    def _flush_exception(self, flush, do_close=True):
         if flush:
             try:
-                return (flush(), False)
+                return (flush(do_close=do_close), False)
             except OSError:
                 if self.adj.log_socket_errors:
                     self.logger.exception("Socket error")
@@ -240,20 +240,20 @@ class HTTPChannel(wasyncore.dispatcher):
 
         return True
 
-    def _flush_some_if_lockable(self):
+    def _flush_some_if_lockable(self, do_close=True):
         # Since our task may be appending to the outbuf, we try to acquire
         # the lock, but we don't block if we can't.
 
         if self.outbuf_lock.acquire(False):
             try:
-                self._flush_some()
+                self._flush_some(do_close=do_close)
 
                 if self.total_outbufs_len < self.adj.outbuf_high_watermark:
                     self.outbuf_lock.notify()
             finally:
                 self.outbuf_lock.release()
 
-    def _flush_some(self):
+    def _flush_some(self, do_close=True):
         # Send as much data as possible to our client
 
         sent = 0
@@ -267,7 +267,7 @@ class HTTPChannel(wasyncore.dispatcher):
 
             while outbuflen > 0:
                 chunk = outbuf.get(self.sendbuf_len)
-                num_sent = self.send(chunk)
+                num_sent = self.send(chunk, do_close=do_close)
 
                 if num_sent:
                     outbuf.skip(num_sent, True)
@@ -374,7 +374,9 @@ class HTTPChannel(wasyncore.dispatcher):
                 self.total_outbufs_len += num_bytes
 
                 if self.total_outbufs_len >= self.adj.send_bytes:
-                    (flushed, exception) = self._flush_exception(self._flush_some)
+                    (flushed, exception) = self._flush_exception(
+                        self._flush_some, do_close=False
+                    )
 
                     if (
                         exception
@@ -392,7 +394,7 @@ class HTTPChannel(wasyncore.dispatcher):
 
         if self.total_outbufs_len > self.adj.outbuf_high_watermark:
             with self.outbuf_lock:
-                (_, exception) = self._flush_exception(self._flush_some)
+                (_, exception) = self._flush_exception(self._flush_some, do_close=False)
 
                 if exception:
                     # An exception happened while flushing, wake up the main
diff --git a/src/waitress/parser.py b/src/waitress/parser.py
index ff16a40..b31b5cc 100644
--- a/src/waitress/parser.py
+++ b/src/waitress/parser.py
@@ -103,7 +103,7 @@ class HTTPRequestParser:
                 # If the headers have ended, and we also have part of the body
                 # message in data we still want to validate we aren't going
                 # over our limit for received headers.
-                self.header_bytes_received += index
+                self.header_bytes_received = index
                 consumed = datalen - (len(s) - index)
             else:
                 self.header_bytes_received += datalen
diff --git a/src/waitress/proxy_headers.py b/src/waitress/proxy_headers.py
index 5d61646..652ca0b 100644
--- a/src/waitress/proxy_headers.py
+++ b/src/waitress/proxy_headers.py
@@ -52,7 +52,7 @@ def proxy_headers_middleware(
                     ex.reason,
                     ex.value,
                 )
-                error = BadRequest('Header "{}" malformed.'.format(ex.header))
+                error = BadRequest(f'Header "{ex.header}" malformed.')
                 return error.wsgi_response(environ, start_response)
 
         # Clear out the untrusted proxy headers
@@ -95,7 +95,7 @@ def parse_proxy_headers(
                 if "." not in forward_hop and (
                     ":" in forward_hop and forward_hop[-1] != "]"
                 ):
-                    forwarded_for.append("[{}]".format(forward_hop))
+                    forwarded_for.append(f"[{forward_hop}]")
                 else:
                     forwarded_for.append(forward_hop)
 
diff --git a/src/waitress/runner.py b/src/waitress/runner.py
index 9a5f0e3..22f70f3 100644
--- a/src/waitress/runner.py
+++ b/src/waitress/runner.py
@@ -198,7 +198,7 @@ RUNNER_PATTERN = re.compile(
 def match(obj_name):
     matches = RUNNER_PATTERN.match(obj_name)
     if not matches:
-        raise ValueError("Malformed application '{}'".format(obj_name))
+        raise ValueError(f"Malformed application '{obj_name}'")
     return matches.group("module"), matches.group("object")
 
 
@@ -223,7 +223,7 @@ def resolve(module_name, object_name):
 
 def show_help(stream, name, error=None):  # pragma: no cover
     if error is not None:
-        print("Error: {}\n".format(error), file=stream)
+        print(f"Error: {error}\n", file=stream)
     print(HELP.format(name), file=stream)
 
 
@@ -239,7 +239,7 @@ def show_exception(stream):
     if args:
         print("It had these arguments: ", file=stream)
         for idx, arg in enumerate(args, start=1):
-            print("{}. {}\n".format(idx, arg), file=stream)
+            print(f"{idx}. {arg}\n", file=stream)
     else:
         print("It had no arguments.", file=stream)
 
@@ -282,11 +282,11 @@ def run(argv=sys.argv, _serve=serve):
     try:
         app = resolve(module, obj_name)
     except ImportError:
-        show_help(sys.stderr, name, "Bad module '{}'".format(module))
+        show_help(sys.stderr, name, f"Bad module '{module}'")
         show_exception(sys.stderr)
         return 1
     except AttributeError:
-        show_help(sys.stderr, name, "Bad object name '{}'".format(obj_name))
+        show_help(sys.stderr, name, f"Bad object name '{obj_name}'")
         show_exception(sys.stderr)
         return 1
     if kw["call"]:
diff --git a/src/waitress/server.py b/src/waitress/server.py
index 55cffe9..0a0f876 100644
--- a/src/waitress/server.py
+++ b/src/waitress/server.py
@@ -157,7 +157,7 @@ class MultiSocketServer:
             l = list(l)
 
             if ":" in l[0]:
-                l[0] = "[{}]".format(l[0])
+                l[0] = f"[{l[0]}]"
 
             self.log_info(format_str.format(*l))
 
diff --git a/src/waitress/task.py b/src/waitress/task.py
index a003919..574532f 100644
--- a/src/waitress/task.py
+++ b/src/waitress/task.py
@@ -57,7 +57,7 @@ class ThreadedTaskDispatcher:
 
     def start_new_thread(self, target, thread_no):
         t = threading.Thread(
-            target=target, name="waitress-{}".format(thread_no), args=(thread_no,)
+            target=target, name=f"waitress-{thread_no}", args=(thread_no,)
         )
         t.daemon = True
         t.start()
@@ -266,7 +266,7 @@ class Task:
 
         self.response_headers = response_headers
 
-        first_line = "HTTP/%s %s" % (self.version, self.status)
+        first_line = f"HTTP/{self.version} {self.status}"
         # 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
@@ -355,7 +355,7 @@ class ErrorTask(Task):
         self.response_headers.append(("Connection", "close"))
         self.close_on_finish = True
         self.content_length = len(body)
-        self.write(body.encode("latin-1"))
+        self.write(body)
 
 
 class WSGITask(Task):
@@ -400,11 +400,11 @@ class WSGITask(Task):
             for k, v in headers:
                 if not k.__class__ is str:
                     raise AssertionError(
-                        "Header name %r is not a string in %r" % (k, (k, v))
+                        f"Header name {k!r} is not a string in {(k, v)!r}"
                     )
                 if not v.__class__ is str:
                     raise AssertionError(
-                        "Header value %r is not a string in %r" % (v, (k, v))
+                        f"Header value {v!r} is not a string in {(k, v)!r}"
                     )
 
                 if "\n" in v or "\r" in v:
diff --git a/src/waitress/trigger.py b/src/waitress/trigger.py
index 49b2034..73ac31c 100644
--- a/src/waitress/trigger.py
+++ b/src/waitress/trigger.py
@@ -106,9 +106,7 @@ class _triggerbase:
                     thunk()
                 except:
                     nil, t, v, tbinfo = wasyncore.compact_traceback()
-                    self.log_info(
-                        "exception in trigger thunk: (%s:%s %s)" % (t, v, tbinfo)
-                    )
+                    self.log_info(f"exception in trigger thunk: ({t}:{v} {tbinfo})")
             self.thunks = []
 
 
diff --git a/src/waitress/utilities.py b/src/waitress/utilities.py
index 6ae4742..164752f 100644
--- a/src/waitress/utilities.py
+++ b/src/waitress/utilities.py
@@ -259,11 +259,11 @@ class Error:
         self.body = body
 
     def to_response(self):
-        status = "%s %s" % (self.code, self.reason)
-        body = "%s\r\n\r\n%s" % (self.reason, self.body)
+        status = f"{self.code} {self.reason}"
+        body = f"{self.reason}\r\n\r\n{self.body}"
         tag = "\r\n\r\n(generated by waitress)"
-        body = body + tag
-        headers = [("Content-Type", "text/plain")]
+        body = (body + tag).encode("utf-8")
+        headers = [("Content-Type", "text/plain; charset=utf-8")]
 
         return status, headers, body
 
diff --git a/src/waitress/wasyncore.py b/src/waitress/wasyncore.py
index 9a68c51..b3459e0 100644
--- a/src/waitress/wasyncore.py
+++ b/src/waitress/wasyncore.py
@@ -328,7 +328,7 @@ class dispatcher:
                 status.append("%s:%d" % self.addr)
             except TypeError:  # pragma: no cover
                 status.append(repr(self.addr))
-        return "<%s at %#x>" % (" ".join(status), id(self))
+        return "<{} at {:#x}>".format(" ".join(status), id(self))
 
     __str__ = __repr__
 
@@ -426,7 +426,7 @@ class dispatcher:
         else:
             return conn, addr
 
-    def send(self, data):
+    def send(self, data, do_close=True):
         try:
             result = self.socket.send(data)
             return result
@@ -434,7 +434,8 @@ class dispatcher:
             if why.args[0] == EWOULDBLOCK:
                 return 0
             elif why.args[0] in _DISCONNECTED:
-                self.handle_close()
+                if do_close:
+                    self.handle_close()
                 return 0
             else:
                 raise
diff --git a/tests/fixtureapps/error_traceback.py b/tests/fixtureapps/error_traceback.py
new file mode 100644
index 0000000..24e4cbf
--- /dev/null
+++ b/tests/fixtureapps/error_traceback.py
@@ -0,0 +1,2 @@
+def app(environ, start_response):  # pragma: no cover
+    raise ValueError("Invalid application: " + chr(8364))
diff --git a/tests/test_channel.py b/tests/test_channel.py
index b1c317d..8467ae7 100644
--- a/tests/test_channel.py
+++ b/tests/test_channel.py
@@ -376,7 +376,7 @@ class TestHTTPChannel(unittest.TestCase):
         inst.total_outbufs_len = len(inst.outbufs[0])
         inst.adj.send_bytes = 1
         inst.adj.outbuf_high_watermark = 2
-        sock.send = lambda x: False
+        sock.send = lambda x, do_close=True: False
         inst.will_close = False
         inst.last_activity = 0
         result = inst.handle_write()
@@ -453,7 +453,7 @@ class TestHTTPChannel(unittest.TestCase):
 
         buf = DummyHugeOutbuffer()
         inst.outbufs = [buf]
-        inst.send = lambda *arg: 0
+        inst.send = lambda *arg, do_close: 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
diff --git a/tests/test_functional.py b/tests/test_functional.py
index 60eb24a..1dfd889 100644
--- a/tests/test_functional.py
+++ b/tests/test_functional.py
@@ -359,7 +359,7 @@ class EchoTests:
                 sorted(headers.keys()),
                 ["connection", "content-length", "content-type", "date", "server"],
             )
-            self.assertEqual(headers["content-type"], "text/plain")
+            self.assertEqual(headers["content-type"], "text/plain; charset=utf-8")
             # connection has been closed
             self.send_check_error(to_send)
             self.assertRaises(ConnectionClosed, read_http, fp)
@@ -381,7 +381,7 @@ class EchoTests:
                 sorted(headers.keys()),
                 ["connection", "content-length", "content-type", "date", "server"],
             )
-            self.assertEqual(headers["content-type"], "text/plain")
+            self.assertEqual(headers["content-type"], "text/plain; charset=utf-8")
             # connection has been closed
             self.send_check_error(to_send)
             self.assertRaises(ConnectionClosed, read_http, fp)
@@ -403,7 +403,7 @@ class EchoTests:
                 sorted(headers.keys()),
                 ["connection", "content-length", "content-type", "date", "server"],
             )
-            self.assertEqual(headers["content-type"], "text/plain")
+            self.assertEqual(headers["content-type"], "text/plain; charset=utf-8")
             # connection has been closed
             self.send_check_error(to_send)
             self.assertRaises(ConnectionClosed, read_http, fp)
@@ -428,7 +428,7 @@ class EchoTests:
                 sorted(headers.keys()),
                 ["connection", "content-length", "content-type", "date", "server"],
             )
-            self.assertEqual(headers["content-type"], "text/plain")
+            self.assertEqual(headers["content-type"], "text/plain; charset=utf-8")
             # connection has been closed
             self.send_check_error(to_send)
             self.assertRaises(ConnectionClosed, read_http, fp)
@@ -1121,7 +1121,7 @@ class TooLargeTests:
             self.assertline(line, "413", "Request Entity Too Large", "HTTP/1.1")
             cl = int(headers["content-length"])
             self.assertEqual(cl, len(response_body))
-            self.assertEqual(headers["content-type"], "text/plain")
+            self.assertEqual(headers["content-type"], "text/plain; charset=utf-8")
             # connection has been closed
             self.send_check_error(to_send)
             self.assertRaises(ConnectionClosed, read_http, fp)
@@ -1269,6 +1269,49 @@ class InternalServerErrorTests:
             self.assertRaises(ConnectionClosed, read_http, fp)
 
 
+class InternalServerErrorTestsWithTraceback:
+    def setUp(self):
+        from tests.fixtureapps import error_traceback
+
+        self.start_subprocess(error_traceback.app, expose_tracebacks=True)
+
+    def tearDown(self):
+        self.stop_subprocess()
+
+    def test_expose_tracebacks_http_10(self):
+        to_send = b"GET / HTTP/1.0\r\n\r\n"
+        self.connect()
+        self.sock.send(to_send)
+        with self.sock.makefile("rb", 0) as fp:
+            line, headers, response_body = read_http(fp)
+            self.assertline(line, "500", "Internal Server Error", "HTTP/1.0")
+            cl = int(headers["content-length"])
+            self.assertEqual(cl, len(response_body))
+            self.assertTrue(response_body.startswith(b"Internal Server Error"))
+            self.assertEqual(headers["connection"], "close")
+            # connection has been closed
+            self.send_check_error(to_send)
+            self.assertRaises(ConnectionClosed, read_http, fp)
+
+    def test_expose_tracebacks_http_11(self):
+        to_send = b"GET / HTTP/1.1\r\n\r\n"
+        self.connect()
+        self.sock.send(to_send)
+        with self.sock.makefile("rb", 0) as fp:
+            line, headers, response_body = read_http(fp)
+            self.assertline(line, "500", "Internal Server Error", "HTTP/1.1")
+            cl = int(headers["content-length"])
+            self.assertEqual(cl, len(response_body))
+            self.assertTrue(response_body.startswith(b"Internal Server Error"))
+            self.assertEqual(
+                sorted(headers.keys()),
+                ["connection", "content-length", "content-type", "date", "server"],
+            )
+            # connection has been closed
+            self.send_check_error(to_send)
+            self.assertRaises(ConnectionClosed, read_http, fp)
+
+
 class FileWrapperTests:
     def setUp(self):
         from tests.fixtureapps import filewrapper
@@ -1538,6 +1581,12 @@ class TcpInternalServerErrorTests(
     pass
 
 
+class TcpInternalServerErrorTestsWithTraceback(
+    InternalServerErrorTestsWithTraceback, TcpTests, unittest.TestCase
+):
+    pass
+
+
 class TcpFileWrapperTests(FileWrapperTests, TcpTests, unittest.TestCase):
     pass
 
@@ -1604,6 +1653,11 @@ if hasattr(socket, "AF_UNIX"):
     ):
         pass
 
+    class UnixInternalServerErrorTestsWithTraceback(
+        InternalServerErrorTestsWithTraceback, UnixTests, unittest.TestCase
+    ):
+        pass
+
     class UnixFileWrapperTests(FileWrapperTests, UnixTests, unittest.TestCase):
         pass
 
diff --git a/tests/test_parser.py b/tests/test_parser.py
index 4461bde..9e9f1cd 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -106,6 +106,18 @@ class TestHTTPRequestParser(unittest.TestCase):
         self.assertTrue(self.parser.completed)
         self.assertTrue(isinstance(self.parser.error, RequestEntityTooLarge))
 
+    def test_received_headers_not_too_large_multiple_chunks(self):
+
+        data = b"GET /foobar HTTP/8.4\r\nX-Foo: 1\r\n"
+        data2 = b"X-Foo-Other: 3\r\n\r\n"
+        self.parser.adj.max_request_header_size = len(data) + len(data2) + 1
+        result = self.parser.received(data)
+        self.assertEqual(result, 32)
+        result = self.parser.received(data2)
+        self.assertEqual(result, 18)
+        self.assertTrue(self.parser.completed)
+        self.assertFalse(self.parser.error)
+
     def test_received_headers_too_large(self):
 
         self.parser.adj.max_request_header_size = 2
diff --git a/tests/test_proxy_headers.py b/tests/test_proxy_headers.py
index 9ed131e..45f9878 100644
--- a/tests/test_proxy_headers.py
+++ b/tests/test_proxy_headers.py
@@ -16,7 +16,7 @@ class TestProxyHeadersMiddleware(unittest.TestCase):
             response.headers = response_headers
 
         response.steps = list(app(environ, start_response))
-        response.body = b"".join(s.encode("latin-1") for s in response.steps)
+        response.body = b"".join(s for s in response.steps)
         return response
 
     def test_get_environment_values_w_scheme_override_untrusted(self):
@@ -727,7 +727,7 @@ class DummyApp:
     def __call__(self, environ, start_response):
         self.environ = environ
         start_response("200 OK", [("Content-Type", "text/plain")])
-        yield "hello"
+        yield b"hello"
 
 
 class DummyResponse:
diff --git a/tests/test_task.py b/tests/test_task.py
index cc579b0..47868e1 100644
--- a/tests/test_task.py
+++ b/tests/test_task.py
@@ -869,7 +869,7 @@ class TestErrorTask(unittest.TestCase):
         self.assertEqual(lines[0], b"HTTP/1.0 432 Too Ugly")
         self.assertEqual(lines[1], b"Connection: close")
         self.assertEqual(lines[2], b"Content-Length: 43")
-        self.assertEqual(lines[3], b"Content-Type: text/plain")
+        self.assertEqual(lines[3], b"Content-Type: text/plain; charset=utf-8")
         self.assertTrue(lines[4])
         self.assertEqual(lines[5], b"Server: waitress")
         self.assertEqual(lines[6], b"Too Ugly")
@@ -885,7 +885,7 @@ class TestErrorTask(unittest.TestCase):
         self.assertEqual(lines[0], b"HTTP/1.1 432 Too Ugly")
         self.assertEqual(lines[1], b"Connection: close")
         self.assertEqual(lines[2], b"Content-Length: 43")
-        self.assertEqual(lines[3], b"Content-Type: text/plain")
+        self.assertEqual(lines[3], b"Content-Type: text/plain; charset=utf-8")
         self.assertTrue(lines[4])
         self.assertEqual(lines[5], b"Server: waitress")
         self.assertEqual(lines[6], b"Too Ugly")
@@ -902,7 +902,7 @@ class TestErrorTask(unittest.TestCase):
         self.assertEqual(lines[0], b"HTTP/1.1 432 Too Ugly")
         self.assertEqual(lines[1], b"Connection: close")
         self.assertEqual(lines[2], b"Content-Length: 43")
-        self.assertEqual(lines[3], b"Content-Type: text/plain")
+        self.assertEqual(lines[3], b"Content-Type: text/plain; charset=utf-8")
         self.assertTrue(lines[4])
         self.assertEqual(lines[5], b"Server: waitress")
         self.assertEqual(lines[6], b"Too Ugly")
@@ -919,7 +919,7 @@ class TestErrorTask(unittest.TestCase):
         self.assertEqual(lines[0], b"HTTP/1.1 432 Too Ugly")
         self.assertEqual(lines[1], b"Connection: close")
         self.assertEqual(lines[2], b"Content-Length: 43")
-        self.assertEqual(lines[3], b"Content-Type: text/plain")
+        self.assertEqual(lines[3], b"Content-Type: text/plain; charset=utf-8")
         self.assertTrue(lines[4])
         self.assertEqual(lines[5], b"Server: waitress")
         self.assertEqual(lines[6], b"Too Ugly")
diff --git a/tests/test_wasyncore.py b/tests/test_wasyncore.py
index 5e0559f..e833c7e 100644
--- a/tests/test_wasyncore.py
+++ b/tests/test_wasyncore.py
@@ -31,7 +31,7 @@ if os.name == "java":  # pragma: no cover
 else:
     TESTFN = "@test"
 
-TESTFN = "{}_{}_tmp".format(TESTFN, os.getpid())
+TESTFN = f"{TESTFN}_{os.getpid()}_tmp"
 
 
 class DummyLogger:  # pragma: no cover
@@ -574,7 +574,7 @@ class HelperFunctionTests(unittest.TestCase):
         self.assertEqual(function, "test_compact_traceback")
         self.assertEqual(t, real_t)
         self.assertEqual(v, real_v)
-        self.assertEqual(info, "[%s|%s|%s]" % (f, function, line))
+        self.assertEqual(info, f"[{f}|{function}|{line}]")
 
 
 class DispatcherTests(unittest.TestCase):

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/lib/python3/dist-packages/waitress-2.1.2.dist-info/METADATA
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/waitress-2.1.2.dist-info/RECORD
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/waitress-2.1.2.dist-info/WHEEL
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/waitress-2.1.2.dist-info/entry_points.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/waitress-2.1.2.dist-info/top_level.txt

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/lib/python3/dist-packages/waitress-2.1.1.dist-info/METADATA
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/waitress-2.1.1.dist-info/RECORD
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/waitress-2.1.1.dist-info/WHEEL
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/waitress-2.1.1.dist-info/entry_points.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/waitress-2.1.1.dist-info/top_level.txt

No differences were encountered between the control files of package python-waitress-doc

No differences were encountered between the control files of package python3-waitress

More details

Full run details