Skip to content

Commit bbb41be

Browse files
committed
gh-102156: Fix HTTPServer timeout ignored in keep-alive connections
1 parent 8665769 commit bbb41be

File tree

2 files changed

+144
-1
lines changed

2 files changed

+144
-1
lines changed

Lib/http/server.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,16 @@ def handle_one_request(self):
428428
429429
"""
430430
try:
431-
self.raw_requestline = self.rfile.readline(65537)
431+
# Set socket timeout for the readline operation to respect server timeout
432+
prev_timeout = self.connection.gettimeout()
433+
if hasattr(self.server, 'timeout') and self.server.timeout is not None:
434+
self.connection.settimeout(self.server.timeout)
435+
436+
try:
437+
self.raw_requestline = self.rfile.readline(65537)
438+
finally:
439+
# Restore previous timeout
440+
self.connection.settimeout(prev_timeout)
432441
if len(self.raw_requestline) > 65536:
433442
self.requestline = ''
434443
self.request_version = ''

Lib/test/test_httpservers.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1122,6 +1122,140 @@ def test_date_time_string(self):
11221122
self.assertEqual(self.handler.date_time_string(timestamp=now), expected)
11231123

11241124

1125+
class HTTPServerTimeoutTestCase(unittest.TestCase):
1126+
"""Test HTTPServer timeout functionality in keep-alive connections.
1127+
1128+
Regression test for Issue #102156: HTTPServer.handle_request() not
1129+
respecting timeout with keep-alive connections.
1130+
"""
1131+
1132+
def setUp(self):
1133+
self._threads = threading_helper.threading_setup()
1134+
self.server_started = threading.Event()
1135+
1136+
def tearDown(self):
1137+
threading_helper.threading_cleanup(*self._threads)
1138+
1139+
def test_timeout_in_keepalive_connections(self):
1140+
"""Test that handle_request respects timeout in keep-alive connections."""
1141+
1142+
class request_handler(NoLogRequestHandler, BaseHTTPRequestHandler):
1143+
def do_GET(self):
1144+
self.send_response(HTTPStatus.OK)
1145+
self.send_header('Content-Type', 'text/plain')
1146+
self.send_header('Connection', 'keep-alive')
1147+
self.end_headers()
1148+
self.wfile.write(b'Hello World!')
1149+
1150+
# Create server with short timeout
1151+
server = HTTPServer(('localhost', 0), request_handler)
1152+
server.timeout = 0.5 # 500ms timeout
1153+
port = server.server_address[1]
1154+
1155+
server_ready = threading.Event()
1156+
client_done = threading.Event()
1157+
1158+
def client_worker():
1159+
"""Client that creates keep-alive connection and waits."""
1160+
# Wait for server to be ready to accept connections
1161+
if not server_ready.wait(timeout=2.0):
1162+
return
1163+
try:
1164+
import socket
1165+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
1166+
sock.connect(('localhost', port))
1167+
# Send first request
1168+
sock.send(b'GET / HTTP/1.1\r\nHost: localhost\r\n'
1169+
b'Connection: keep-alive\r\n\r\n')
1170+
sock.recv(1024) # Receive response
1171+
client_done.set() # Signal that first request is complete
1172+
# Keep connection open longer than timeout
1173+
time.sleep(1.0)
1174+
except Exception:
1175+
pass # Expected if connection times out
1176+
1177+
# Start client thread
1178+
client_thread = threading.Thread(target=client_worker, daemon=True)
1179+
client_thread.start()
1180+
1181+
# Signal that server is ready and test handle_request with timeout
1182+
server_ready.set()
1183+
start_time = time.time()
1184+
server.handle_request()
1185+
duration = time.time() - start_time
1186+
1187+
server.server_close()
1188+
1189+
# Should complete within reasonable time (not hang indefinitely)
1190+
# Allow some buffer for timing variations
1191+
self.assertLess(duration, 1.0,
1192+
"handle_request should timeout, not hang indefinitely")
1193+
self.assertGreater(duration, 0.4,
1194+
"handle_request should wait for timeout period")
1195+
1196+
def test_timeout_with_no_requests(self):
1197+
"""Test that handle_request times out when no requests are made."""
1198+
1199+
class request_handler(NoLogRequestHandler, BaseHTTPRequestHandler):
1200+
def do_GET(self):
1201+
self.send_response(HTTPStatus.OK)
1202+
self.end_headers()
1203+
1204+
server = HTTPServer(('localhost', 0), request_handler)
1205+
server.timeout = 0.5 # 500ms timeout
1206+
1207+
start_time = time.time()
1208+
server.handle_request()
1209+
duration = time.time() - start_time
1210+
1211+
server.server_close()
1212+
1213+
# Should timeout after approximately 0.5 seconds
1214+
self.assertGreaterEqual(duration, 0.4)
1215+
self.assertLess(duration, 0.7)
1216+
1217+
def test_normal_request_with_timeout(self):
1218+
"""Test that normal requests complete quickly despite timeout setting."""
1219+
1220+
class request_handler(NoLogRequestHandler, BaseHTTPRequestHandler):
1221+
def do_GET(self):
1222+
self.send_response(HTTPStatus.OK)
1223+
self.end_headers()
1224+
1225+
server = HTTPServer(('localhost', 0), request_handler)
1226+
server.timeout = 1.0 # 1 second timeout
1227+
port = server.server_address[1]
1228+
1229+
server_ready = threading.Event()
1230+
1231+
def make_request():
1232+
# Wait for server to be ready to accept connections
1233+
if not server_ready.wait(timeout=2.0):
1234+
return
1235+
try:
1236+
import socket
1237+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
1238+
sock.connect(('localhost', port))
1239+
sock.send(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1240+
sock.recv(1024)
1241+
except Exception:
1242+
pass
1243+
1244+
client_thread = threading.Thread(target=make_request, daemon=True)
1245+
client_thread.start()
1246+
1247+
# Signal that server is ready and test normal request handling
1248+
server_ready.set()
1249+
start_time = time.time()
1250+
server.handle_request()
1251+
duration = time.time() - start_time
1252+
1253+
server.server_close()
1254+
1255+
# Should complete quickly when request is made
1256+
self.assertLess(duration, 0.5, "Normal requests should complete quickly")
1257+
1258+
11251259
class SimpleHTTPRequestHandlerTestCase(unittest.TestCase):
11261260
""" Test url parsing """
11271261
def setUp(self):

0 commit comments

Comments
 (0)