From face133a62289ef0be58706f68996e111e45d723 Mon Sep 17 00:00:00 2001 From: jonathan vanasco Date: Fri, 20 Mar 2026 16:29:26 -0400 Subject: [PATCH 1/6] Enabled logging of failed port bind in `waysyncore.py:dispatcher.bind` to aid in debugging application bind failures. See https://github.com/Pylons/waitress/issues/471 --- CHANGES.txt | 5 +++++ src/waitress/wasyncore.py | 8 +++++++- tests/test_server.py | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index c7f32ea4..3163b252 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -19,6 +19,11 @@ Bugfix https://github.com/Pylons/waitress/pull/475 and https://github.com/Pylons/waitress/issues/464 +- Enabled logging of failed port bind in `waysyncore.py:dispatcher.bind` to aid + in debugging application bind failures. See + https://github.com/Pylons/waitress/issues/471 + + 3.0.2 (2024-11-16) ------------------ diff --git a/src/waitress/wasyncore.py b/src/waitress/wasyncore.py index 79a593bf..e03364fd 100644 --- a/src/waitress/wasyncore.py +++ b/src/waitress/wasyncore.py @@ -370,8 +370,14 @@ def listen(self, num): return self.socket.listen(num) def bind(self, addr): + # self.logger.log(logging.DEBUG, "Attempting to bind to: %s" % addr) self.addr = addr - return self.socket.bind(addr) + try: + return self.socket.bind(addr) + except Exception as exc: + self.logger.log(logging.CRITICAL, "Failed bind to: %s" % str(addr)) + self.logger.log(logging.CRITICAL, "Exception raised: %s" % str(exc)) + raise def accept(self): # XXX can return either an address pair or None diff --git a/tests/test_server.py b/tests/test_server.py index cede49a7..27ae8330 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1,5 +1,6 @@ import errno import socket +import sys import unittest dummy_app = object() @@ -311,6 +312,42 @@ def test_create_with_one_socket_handle_accept_noerror(self): self.assertListEqual(innersock.opts, [("level", "optname", "value")]) self.assertListEqual(L, [(inst, innersock, None, inst.adj)]) + @unittest.skipIf( + sys.platform.startswith("win"), "This test is not supported on Windows" + ) + def test_port_bind_failure_logging(self): + # ensure the address is logged on a failed port bind + + # create a first app correctly + inst_a = self._makeOne(port=8080) + + # Ensure a second app correctly binds to a different host+port + inst_b = self._makeOne(port=8081) + + # a third app should fail the bind to the fist app's host+port + with self.assertLogs("waitress", level="ERROR") as cm_log: + with self.assertRaises(OSError) as cm: + inst_c = self._makeOne(port=8080) + self.assertTrue( + ("[Errno 48] Address already in use" == str(cm.exception)) + or ("[Errno 98] Address already in use" == str(cm.exception)) + ) + + self.assertIn( + "CRITICAL:waitress:Failed bind to: ('127.0.0.1', 8080)", + cm_log.output, + ) + self.assertTrue( + ( + "CRITICAL:waitress:Exception raised: [Errno 48] Address already in use" + in cm_log.output + ) + or ( + "CRITICAL:waitress:Exception raised: [Errno 98] Address already in use" + in cm_log.output + ) + ) + if hasattr(socket, "AF_UNIX"): From 698cfc85381f4662e7281b487f029306811a24f5 Mon Sep 17 00:00:00 2001 From: jonathan vanasco Date: Mon, 23 Mar 2026 14:10:48 -0400 Subject: [PATCH 2/6] cloud tests for some changes --- src/waitress/wasyncore.py | 4 +-- tests/test_server.py | 52 ++++++++++++++++++++++++++------------- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/waitress/wasyncore.py b/src/waitress/wasyncore.py index e03364fd..dc5761ee 100644 --- a/src/waitress/wasyncore.py +++ b/src/waitress/wasyncore.py @@ -370,13 +370,11 @@ def listen(self, num): return self.socket.listen(num) def bind(self, addr): - # self.logger.log(logging.DEBUG, "Attempting to bind to: %s" % addr) self.addr = addr try: return self.socket.bind(addr) except Exception as exc: - self.logger.log(logging.CRITICAL, "Failed bind to: %s" % str(addr)) - self.logger.log(logging.CRITICAL, "Exception raised: %s" % str(exc)) + self.logger.critical("Failed bind to: `%s` : `%s`" % (str(addr), str(exc))) raise def accept(self): diff --git a/tests/test_server.py b/tests/test_server.py index 27ae8330..fb9577a6 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1,12 +1,28 @@ import errno import socket import sys +from typing import TYPE_CHECKING, List, Union import unittest +if TYPE_CHECKING: + from waitress.server import BaseWSGIServer, MultiSocketServer + dummy_app = object() class TestWSGIServer(unittest.TestCase): + + # most tests only start a single server (for cleanup) + inst: Union["BaseWSGIServer", "MultiSocketServer", None] + + # maintain a list of instantiated servers (for cleanup) + insts: List[Union["BaseWSGIServer", "MultiSocketServer"]] + + def __init__(self, *args, **kwargs): + # override base `__init__` to track `self.insts` for `self.tearDown` + super(TestWSGIServer, self).__init__(*args, **kwargs) + self.insts = [] + def _makeOne( self, application=dummy_app, @@ -30,6 +46,7 @@ def _makeOne( _start=_start, _sock=_sock, ) + self.insts.append(self.inst) return self.inst def _makeOneWithMap( @@ -92,9 +109,15 @@ def _makeWithSockets( return self.inst def tearDown(self): + # most tests only start a single server if self.inst is not None: self.inst.close() + # use the list of instantiated servers + if self.insts: + for inst in self.insts: + inst.close() + def test_ctor_app_is_None(self): self.inst = None self.assertRaises(ValueError, self._makeOneWithMap, app=None) @@ -313,10 +336,15 @@ def test_create_with_one_socket_handle_accept_noerror(self): self.assertListEqual(L, [(inst, innersock, None, inst.adj)]) @unittest.skipIf( - sys.platform.startswith("win"), "This test is not supported on Windows" + sys.platform.startswith("win"), + "Windows doesn't raise an exception when reusing a port", ) def test_port_bind_failure_logging(self): - # ensure the address is logged on a failed port bind + """ + Ensure the address is logged on a failed port bind. + + This test was developed for #471 - logging a failed bind. + """ # create a first app correctly inst_a = self._makeOne(port=8080) @@ -328,25 +356,15 @@ def test_port_bind_failure_logging(self): with self.assertLogs("waitress", level="ERROR") as cm_log: with self.assertRaises(OSError) as cm: inst_c = self._makeOne(port=8080) - self.assertTrue( - ("[Errno 48] Address already in use" == str(cm.exception)) - or ("[Errno 98] Address already in use" == str(cm.exception)) - ) + self.assertEqual(cm.exception.errno, errno.EADDRINUSE) + # check log for inst_c failure self.assertIn( - "CRITICAL:waitress:Failed bind to: ('127.0.0.1', 8080)", + "CRITICAL:waitress:" + "Failed bind to: `('127.0.0.1', 8080)` : " + "`[Errno %s] Address already in use`" % errno.EADDRINUSE, cm_log.output, ) - self.assertTrue( - ( - "CRITICAL:waitress:Exception raised: [Errno 48] Address already in use" - in cm_log.output - ) - or ( - "CRITICAL:waitress:Exception raised: [Errno 98] Address already in use" - in cm_log.output - ) - ) if hasattr(socket, "AF_UNIX"): From 3dc565e852630159560a16d30a56455079da4cb8 Mon Sep 17 00:00:00 2001 From: jonathan vanasco Date: Mon, 23 Mar 2026 15:12:21 -0400 Subject: [PATCH 3/6] addressed comments --- src/waitress/wasyncore.py | 2 +- tests/test_server.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/waitress/wasyncore.py b/src/waitress/wasyncore.py index dc5761ee..3aaa53f7 100644 --- a/src/waitress/wasyncore.py +++ b/src/waitress/wasyncore.py @@ -374,7 +374,7 @@ def bind(self, addr): try: return self.socket.bind(addr) except Exception as exc: - self.logger.critical("Failed bind to: `%s` : `%s`" % (str(addr), str(exc))) + self.logger.critical("Failed bind to: `%s` : `%s`", str(addr), exc) raise def accept(self): diff --git a/tests/test_server.py b/tests/test_server.py index fb9577a6..6a221c1a 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1,4 +1,5 @@ import errno +import os import socket import sys from typing import TYPE_CHECKING, List, Union @@ -362,7 +363,7 @@ def test_port_bind_failure_logging(self): self.assertIn( "CRITICAL:waitress:" "Failed bind to: `('127.0.0.1', 8080)` : " - "`[Errno %s] Address already in use`" % errno.EADDRINUSE, + f"`[Errno {errno.EADDRINUSE}] {os.strerror(errno.EADDRINUSE)}`", cm_log.output, ) From b051c013361b27940009c6d7506e18d1c9afdb3b Mon Sep 17 00:00:00 2001 From: jonathan vanasco Date: Mon, 23 Mar 2026 17:30:29 -0400 Subject: [PATCH 4/6] test harness updates --- tests/test_server.py | 46 +++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/tests/test_server.py b/tests/test_server.py index 6a221c1a..3ac85616 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -6,16 +6,12 @@ import unittest if TYPE_CHECKING: - from waitress.server import BaseWSGIServer, MultiSocketServer + from waitress.server import BaseWSGIServer, MultiSocketServer, UnixWSGIServer dummy_app = object() - class TestWSGIServer(unittest.TestCase): - # most tests only start a single server (for cleanup) - inst: Union["BaseWSGIServer", "MultiSocketServer", None] - # maintain a list of instantiated servers (for cleanup) insts: List[Union["BaseWSGIServer", "MultiSocketServer"]] @@ -38,7 +34,7 @@ def _makeOne( ): from waitress.server import create_server - self.inst = create_server( + inst = create_server( application, host=host, port=port, @@ -47,8 +43,8 @@ def _makeOne( _start=_start, _sock=_sock, ) - self.insts.append(self.inst) - return self.inst + self.insts.append(inst) + return inst def _makeOneWithMap( self, adj=None, _start=True, host="127.0.0.1", port=0, app=dummy_app @@ -74,7 +70,7 @@ def _makeOneWithMulti( map = {} from waitress.server import create_server - self.inst = create_server( + inst = create_server( app, listen=listen, map=map, @@ -82,7 +78,8 @@ def _makeOneWithMulti( _start=_start, _sock=sock, ) - return self.inst + self.insts.append(inst) + return inst def _makeWithSockets( self, @@ -99,7 +96,7 @@ def _makeWithSockets( _sockets = [] if sockets is not None: _sockets = sockets - self.inst = create_server( + inst = create_server( application, map=map, _dispatcher=_dispatcher, @@ -107,20 +104,15 @@ def _makeWithSockets( _sock=_sock, sockets=_sockets, ) - return self.inst + self.insts.append(inst) + return inst def tearDown(self): - # most tests only start a single server - if self.inst is not None: - self.inst.close() - - # use the list of instantiated servers - if self.insts: - for inst in self.insts: - inst.close() + # iterate the list of instantiated servers and `close()` them + for inst in self.insts: + inst.close() def test_ctor_app_is_None(self): - self.inst = None self.assertRaises(ValueError, self._makeOneWithMap, app=None) def test_ctor_start_true(self): @@ -288,10 +280,13 @@ def test_backward_compatibility(self): from waitress.server import TcpWSGIServer, WSGIServer self.assertIs(WSGIServer, TcpWSGIServer) - self.inst = WSGIServer(None, _start=False, port=1234) + inst = WSGIServer(None, _start=False, port=1234) # Ensure the adjustment was actually applied. - self.assertNotEqual(Adjustments.port, 1234) - self.assertEqual(self.inst.adj.port, 1234) + try: + self.assertNotEqual(Adjustments.port, 1234) + self.assertEqual(inst.adj.port, 1234) + finally: + inst.close() def test_create_with_one_tcp_socket(self): from waitress.server import TcpWSGIServer @@ -373,6 +368,9 @@ def test_port_bind_failure_logging(self): class TestUnixWSGIServer(unittest.TestCase): unix_socket = "/tmp/waitress.test.sock" + # maintain a list of instantiated servers (for cleanup) + inst: List[Union["BaseWSGIServer", "MultiSocketServer", "UnixWSGIServer"]] + def _makeOne(self, _start=True, _sock=None): from waitress.server import create_server From bb58cd2cf11a42cb43a24b2f48a152cb6c2382ee Mon Sep 17 00:00:00 2001 From: jonathan vanasco Date: Mon, 23 Mar 2026 17:32:23 -0400 Subject: [PATCH 5/6] I think there was a bump on the linter version --- tests/test_server.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_server.py b/tests/test_server.py index 3ac85616..21db84dd 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -10,6 +10,7 @@ dummy_app = object() + class TestWSGIServer(unittest.TestCase): # maintain a list of instantiated servers (for cleanup) From a0e41df5c3056e88d66bab46043a950d989fbe78 Mon Sep 17 00:00:00 2001 From: jonathan vanasco Date: Mon, 23 Mar 2026 17:49:14 -0400 Subject: [PATCH 6/6] found the socket issue --- tests/test_server.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_server.py b/tests/test_server.py index 21db84dd..d0a49e00 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -341,6 +341,8 @@ def test_port_bind_failure_logging(self): Ensure the address is logged on a failed port bind. This test was developed for #471 - logging a failed bind. + + This test will leave a socket open until #480 is fixed. """ # create a first app correctly @@ -352,6 +354,7 @@ def test_port_bind_failure_logging(self): # a third app should fail the bind to the fist app's host+port with self.assertLogs("waitress", level="ERROR") as cm_log: with self.assertRaises(OSError) as cm: + # this line will trigger #480 and leave a socket open inst_c = self._makeOne(port=8080) self.assertEqual(cm.exception.errno, errno.EADDRINUSE)