88import threading
99import time
1010import urllib .parse
11+ import warnings
1112from collections import defaultdict
1213from collections .abc import Iterable
1314from collections .abc import Mapping
@@ -612,6 +613,8 @@ class HTTPServerBase(abc.ABC): # pylint: disable=too-many-instance-attributes
612613 :param port: the TCP port where the server will listen
613614 :param ssl_context: the ssl context object to use for https connections
614615 :param threaded: whether to handle concurrent requests in separate threads
616+ :param startup_timeout: maximum time in seconds to wait for server readiness.
617+ Set to ``None`` to disable readiness waiting.
615618
616619 .. py:attribute:: log
617620
@@ -635,6 +638,7 @@ def __init__(
635638 ssl_context : SSLContext | None = None ,
636639 * ,
637640 threaded : bool = False ,
641+ startup_timeout : float | None = 10.0 ,
638642 ):
639643 """
640644 Initializes the instance.
@@ -649,6 +653,7 @@ def __init__(
649653 self .log : list [tuple [Request , Response ]] = []
650654 self .ssl_context = ssl_context
651655 self .threaded = threaded
656+ self .startup_timeout = startup_timeout
652657 self .no_handler_status_code = 500
653658 self ._server_ready_event : threading .Event = threading .Event ()
654659
@@ -732,8 +737,10 @@ def thread_target(self):
732737
733738 This should not be called directly, but can be overridden to tailor it to your needs.
734739
735- If overriding, you must call ``self._server_ready_event.set()`` before starting
736- to serve requests, otherwise :py:meth:`start` will raise an error after timeout.
740+ If overriding, you should call ``self._server_ready_event.set()`` before starting
741+ to serve requests. If the event is not set within the timeout, :py:meth:`start`
742+ will emit a warning if the thread is still alive; if the thread dies during
743+ startup, :py:meth:`start` raises an error.
737744 """
738745 assert self .server is not None
739746 self ._server_ready_event .set ()
@@ -776,22 +783,33 @@ def start(self) -> None:
776783
777784 self .port = self .server .port # Update port (needed if `port` was set to 0)
778785 # Explicitly make the new thread daemonic to avoid shutdown issues
779- self ._server_ready_event .clear ()
786+ # Create a new event for each startup to prevent stale threads from
787+ # signaling readiness for a subsequent start() attempt.
788+ self ._server_ready_event = threading .Event ()
780789 self .server_thread = threading .Thread (target = self .thread_target , daemon = True )
781790 self .server_thread .start ()
782- if not self ._server_ready_event .wait (timeout = 10 ):
783- # Clean up the server before raising.
784- # Use server_close() instead of shutdown() to avoid deadlock
785- # if serve_forever() was never called.
786- self .server .server_close ()
787- self .server_thread .join (timeout = 5 )
788- self .server = None
789- self .server_thread = None
790- raise HTTPServerError (
791- "Server did not start within timeout. "
792- "If you override thread_target(), ensure it calls "
793- "self._server_ready_event.set() before serving."
794- )
791+ if self .startup_timeout is not None and not self ._server_ready_event .wait (timeout = self .startup_timeout ):
792+ # Event was not set within timeout.
793+ # Check if thread is still alive (custom thread_target may not set the event)
794+ if self .server_thread .is_alive ():
795+ # Server thread is running, assume it's working (backward compatibility)
796+ warnings .warn (
797+ "Server thread is running but ready event was not set. "
798+ "If you override thread_target(), call self._server_ready_event.set() "
799+ "before serving to ensure reliable startup." ,
800+ stacklevel = 2 ,
801+ )
802+ else :
803+ # Thread died, clean up and raise
804+ self .server .server_close ()
805+ self .server_thread .join (timeout = 5 )
806+ self .server = None
807+ self .server_thread = None
808+ raise HTTPServerError (
809+ "Server thread died during startup. "
810+ "If you override thread_target(), ensure it calls "
811+ "self._server_ready_event.set() before serving."
812+ )
795813
796814 def stop (self ):
797815 """
@@ -953,6 +971,8 @@ class HTTPServer(HTTPServerBase): # pylint: disable=too-many-instance-attribute
953971 manager
954972
955973 :param threaded: whether to handle concurrent requests in separate threads
974+ :param startup_timeout: maximum time in seconds to wait for server readiness.
975+ Set to ``None`` to disable readiness waiting.
956976
957977 .. py:attribute:: no_handler_status_code
958978
@@ -972,11 +992,18 @@ def __init__(
972992 default_waiting_settings : WaitingSettings | None = None ,
973993 * ,
974994 threaded : bool = False ,
995+ startup_timeout : float | None = 10.0 ,
975996 ):
976997 """
977998 Initializes the instance.
978999 """
979- super ().__init__ (host , port , ssl_context , threaded = threaded )
1000+ super ().__init__ (
1001+ host ,
1002+ port ,
1003+ ssl_context ,
1004+ threaded = threaded ,
1005+ startup_timeout = startup_timeout ,
1006+ )
9801007
9811008 self .ordered_handlers : list [RequestHandler ] = []
9821009 self .oneshot_handlers = RequestHandlerList ()
0 commit comments