88import threading
99import time
1010import urllib .parse
11+ import warnings
1112from collections import defaultdict
1213from collections .abc import Callable
1314from collections .abc import Generator
@@ -615,6 +616,8 @@ class HTTPServerBase(abc.ABC): # pylint: disable=too-many-instance-attributes
615616 :param port: the TCP port where the server will listen
616617 :param ssl_context: the ssl context object to use for https connections
617618 :param threaded: whether to handle concurrent requests in separate threads
619+ :param startup_timeout: maximum time in seconds to wait for server readiness.
620+ Set to ``None`` to disable readiness waiting.
618621
619622 .. py:attribute:: log
620623
@@ -638,6 +641,7 @@ def __init__(
638641 ssl_context : SSLContext | None = None ,
639642 * ,
640643 threaded : bool = False ,
644+ startup_timeout : float | None = 10.0 ,
641645 ) -> None :
642646 """
643647 Initializes the instance.
@@ -652,6 +656,7 @@ def __init__(
652656 self .log : list [tuple [Request , Response ]] = []
653657 self .ssl_context = ssl_context
654658 self .threaded = threaded
659+ self .startup_timeout = startup_timeout
655660 self .no_handler_status_code = 500
656661 self ._server_ready_event : threading .Event = threading .Event ()
657662
@@ -735,8 +740,10 @@ def thread_target(self) -> None:
735740
736741 This should not be called directly, but can be overridden to tailor it to your needs.
737742
738- If overriding, you must call ``self._server_ready_event.set()`` before starting
739- to serve requests, otherwise :py:meth:`start` will raise an error after timeout.
743+ If overriding, you should call ``self._server_ready_event.set()`` before starting
744+ to serve requests. If the event is not set within the timeout, :py:meth:`start`
745+ will emit a warning if the thread is still alive; if the thread dies during
746+ startup, :py:meth:`start` raises an error.
740747 """
741748 assert self .server is not None
742749 self ._server_ready_event .set ()
@@ -779,22 +786,33 @@ def start(self) -> None:
779786
780787 self .port = self .server .port # Update port (needed if `port` was set to 0)
781788 # Explicitly make the new thread daemonic to avoid shutdown issues
782- self ._server_ready_event .clear ()
789+ # Create a new event for each startup to prevent stale threads from
790+ # signaling readiness for a subsequent start() attempt.
791+ self ._server_ready_event = threading .Event ()
783792 self .server_thread = threading .Thread (target = self .thread_target , daemon = True )
784793 self .server_thread .start ()
785- if not self ._server_ready_event .wait (timeout = 10 ):
786- # Clean up the server before raising.
787- # Use server_close() instead of shutdown() to avoid deadlock
788- # if serve_forever() was never called.
789- self .server .server_close ()
790- self .server_thread .join (timeout = 5 )
791- self .server = None
792- self .server_thread = None
793- raise HTTPServerError (
794- "Server did not start within timeout. "
795- "If you override thread_target(), ensure it calls "
796- "self._server_ready_event.set() before serving."
797- )
794+ if self .startup_timeout is not None and not self ._server_ready_event .wait (timeout = self .startup_timeout ):
795+ # Event was not set within timeout.
796+ # Check if thread is still alive (custom thread_target may not set the event)
797+ if self .server_thread .is_alive ():
798+ # Server thread is running, assume it's working (backward compatibility)
799+ warnings .warn (
800+ "Server thread is running but ready event was not set. "
801+ "If you override thread_target(), call self._server_ready_event.set() "
802+ "before serving to ensure reliable startup." ,
803+ stacklevel = 2 ,
804+ )
805+ else :
806+ # Thread died, clean up and raise
807+ self .server .server_close ()
808+ self .server_thread .join (timeout = 5 )
809+ self .server = None
810+ self .server_thread = None
811+ raise HTTPServerError (
812+ "Server thread died during startup. "
813+ "If you override thread_target(), ensure it calls "
814+ "self._server_ready_event.set() before serving."
815+ )
798816
799817 def stop (self ) -> None :
800818 """
@@ -956,6 +974,8 @@ class HTTPServer(HTTPServerBase): # pylint: disable=too-many-instance-attribute
956974 manager
957975
958976 :param threaded: whether to handle concurrent requests in separate threads
977+ :param startup_timeout: maximum time in seconds to wait for server readiness.
978+ Set to ``None`` to disable readiness waiting.
959979
960980 .. py:attribute:: no_handler_status_code
961981
@@ -975,11 +995,18 @@ def __init__(
975995 default_waiting_settings : WaitingSettings | None = None ,
976996 * ,
977997 threaded : bool = False ,
998+ startup_timeout : float | None = 10.0 ,
978999 ) -> None :
9791000 """
9801001 Initializes the instance.
9811002 """
982- super ().__init__ (host , port , ssl_context , threaded = threaded )
1003+ super ().__init__ (
1004+ host ,
1005+ port ,
1006+ ssl_context ,
1007+ threaded = threaded ,
1008+ startup_timeout = startup_timeout ,
1009+ )
9831010
9841011 self .ordered_handlers : list [RequestHandler ] = []
9851012 self .oneshot_handlers = RequestHandlerList ()
0 commit comments