55import time
66
77import pytest
8+ import requests
9+ from requests .exceptions import Timeout
810
911from pytest_httpserver import HTTPServer
1012
1113
12- def test_server_ready_immediately_after_start ():
14+ def test_server_ready_immediately_after_start () -> None :
1315 """Test that the server accepts connections immediately after start() returns."""
1416 server = HTTPServer (host = "localhost" , port = 0 )
1517 server .expect_request ("/" ).respond_with_data ("ok" )
@@ -22,7 +24,7 @@ def test_server_ready_immediately_after_start():
2224 server .stop ()
2325
2426
25- def test_server_ready_under_load ():
27+ def test_server_ready_under_load () -> None :
2628 """Test that the server is ready even when started multiple times in succession."""
2729 for _ in range (10 ):
2830 server = HTTPServer (host = "localhost" , port = 0 )
@@ -38,7 +40,7 @@ def test_server_ready_under_load():
3840class SlowStartServer (HTTPServer ):
3941 """A server subclass that simulates slow startup."""
4042
41- def thread_target (self ):
43+ def thread_target (self ) -> None :
4244 time .sleep (0.5 ) # Simulate slow initialization
4345 self ._server_ready_event .set ()
4446 assert self .server is not None
@@ -48,19 +50,19 @@ def thread_target(self):
4850class NoReadyEventServer (HTTPServer ):
4951 """A server subclass that never signals readiness."""
5052
51- def thread_target (self ):
53+ def thread_target (self ) -> None :
5254 assert self .server is not None
5355 self .server .serve_forever ()
5456
5557
56- def test_slow_start_server_waits_for_ready ():
58+ def test_slow_start_server_waits_for_ready () -> None :
5759 """Test that start() waits for slow thread_target implementations."""
5860 server = SlowStartServer (host = "localhost" , port = 0 )
5961 server .expect_request ("/" ).respond_with_data ("ok" )
6062
61- start_time = time .time ()
63+ start_time = time .monotonic ()
6264 server .start ()
63- elapsed = time .time () - start_time
65+ elapsed = time .monotonic () - start_time
6466
6567 try :
6668 # Should have waited at least 0.5 seconds
@@ -72,7 +74,7 @@ def test_slow_start_server_waits_for_ready():
7274 server .stop ()
7375
7476
75- def test_new_event_created_for_each_start ():
77+ def test_new_event_created_for_each_start () -> None :
7678 """Test that a new event is created for each start() to isolate retries."""
7779 server = HTTPServer (host = "localhost" , port = 0 )
7880 server .expect_request ("/" ).respond_with_data ("ok" )
@@ -92,7 +94,7 @@ def test_new_event_created_for_each_start():
9294 assert second_start_event is not first_start_event
9395
9496
95- def test_warns_when_ready_event_not_set ():
97+ def test_warns_when_ready_event_not_set () -> None :
9698 """Test that a warning is emitted when the ready event is never set."""
9799 server = NoReadyEventServer (host = "localhost" , port = 0 , startup_timeout = 0.0 )
98100 server .expect_request ("/" ).respond_with_data ("ok" )
@@ -112,3 +114,75 @@ def test_warns_when_ready_event_not_set():
112114 raise AssertionError ("Server did not accept connections within 1 second" )
113115 finally :
114116 server .stop ()
117+
118+
119+ class SlowServeServer (HTTPServer ):
120+ """A server that delays serve_forever() but does not set ready event early.
121+
122+ This simulates the scenario where:
123+ - bind() and listen() complete (TCP connections queue in backlog)
124+ - But serve_forever() hasn't started yet (no HTTP responses)
125+ """
126+
127+ def thread_target (self ) -> None :
128+ assert self .server is not None
129+ # Delay before serve_forever - connections will queue but not be processed
130+ time .sleep (3.0 )
131+ self ._server_ready_event .set ()
132+ self .server .serve_forever ()
133+
134+
135+ def test_http_request_fails_before_serve_forever_without_wait () -> None :
136+ """
137+ Demonstrate the race condition: TCP connects but HTTP times out.
138+
139+ This test shows why waiting for server readiness matters:
140+ - After start(), TCP connections succeed (queued in backlog)
141+ - But HTTP requests timeout because serve_forever() hasn't started
142+ - With short client timeouts (common in production), this causes failures
143+ """
144+ # Use startup_timeout=0 to NOT wait for ready event (old behavior)
145+ server = SlowServeServer (host = "localhost" , port = 0 , startup_timeout = 0.0 )
146+ server .expect_request ("/ping" ).respond_with_data ("pong" )
147+
148+ with pytest .warns (UserWarning , match = "ready event was not set" ):
149+ server .start ()
150+
151+ try :
152+ # TCP connection succeeds (proves Zsolt's point about backlog)
153+ sock = socket .create_connection ((server .host , server .port ), timeout = 1 )
154+ sock .close ()
155+
156+ # But HTTP request with short timeout fails!
157+ # This is the actual problem in containerized environments
158+ with pytest .raises (Timeout ):
159+ requests .get (server .url_for ("/ping" ), timeout = (0.5 , 0.5 ))
160+ finally :
161+ server .stop ()
162+
163+
164+ def test_http_request_succeeds_when_waiting_for_ready () -> None :
165+ """
166+ Demonstrate that waiting for ready event fixes the race condition.
167+
168+ With startup_timeout enabled (default), start() waits until
169+ serve_forever() begins, so HTTP requests succeed immediately.
170+ """
171+ # Use default startup_timeout to wait for ready event
172+ server = SlowServeServer (host = "localhost" , port = 0 ) # default startup_timeout=10.0
173+ server .expect_request ("/ping" ).respond_with_data ("pong" )
174+
175+ start_time = time .monotonic ()
176+ server .start ()
177+ elapsed = time .monotonic () - start_time
178+
179+ try :
180+ # Should have waited for the slow startup
181+ assert elapsed >= 3.0 , f"Expected to wait >= 3.0s, but only waited { elapsed } s"
182+
183+ # HTTP request succeeds because serve_forever() has started
184+ response = requests .get (server .url_for ("/ping" ), timeout = (0.5 , 0.5 ))
185+ assert response .status_code == 200
186+ assert response .text == "pong"
187+ finally :
188+ server .stop ()
0 commit comments