fix(server): clean up dead socket code and fix macOS keepalive (closes #1583)

- Delete QuietHTTPServer.server_bind() override entirely:
  TCP_KEEP* setsockopts on the listening socket are no-ops without
  SO_KEEPALIVE, and SO_REUSEADDR=1 is already set by the parent class.
  The actual fix lives entirely in Handler.setup().

- Restructure Handler.setup() with per-platform branches so
  SO_KEEPALIVE=1 is always applied before timing params, and macOS
  (TCP_KEEPALIVE) gets keepalive instead of aborting on TCP_KEEPIDLE.
This commit is contained in:
Frank Song 2026-05-04 16:35:42 +08:00
parent 9986d2fd30
commit 26208e46ae

View file

@ -27,19 +27,6 @@ class QuietHTTPServer(ThreadingHTTPServer):
daemon_threads = True
request_queue_size = 64
def server_bind(self):
"""Set socket options to prevent TIME_WAIT and CLOSE-WAIT accumulation."""
# Enable address reuse to avoid "Address already in use" errors
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Enable TCP keepalive to detect dead connections (Linux)
try:
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60) # Start probing after 60s idle
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10) # Probe every 10s
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3) # Drop after 3 failed probes
except (OSError, AttributeError):
pass # TCP_KEEP* may not be available on all platforms
super().server_bind()
def handle_error(self, request, client_address):
"""Override to suppress logging for common client disconnect errors."""
exc_type, exc_value, _ = sys.exc_info()
@ -63,19 +50,31 @@ class Handler(BaseHTTPRequestHandler):
timeout = 30 # seconds — kills idle/incomplete connections to prevent thread exhaustion
def setup(self):
"""Set additional socket options for each connection."""
"""Set socket options for each accepted connection."""
super().setup()
# Enable TCP keepalive on the connection socket (not just server socket)
# TCP_NODELAY — universal, disables Nagle for HTTP latency
try:
self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
except OSError:
pass
# SO_KEEPALIVE — universal master switch (must be set before timing params)
try:
import socket
self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # Disable Nagle's algorithm
# Aggressive keepalive: start after 10s idle, probe every 5s, drop after 3 failures
self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 10)
self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 5)
self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3)
self.connection.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
except (OSError, AttributeError):
pass # May not be available on all platforms
except OSError:
pass
# Per-platform timing parameters
if hasattr(socket, 'TCP_KEEPIDLE'): # Linux
try:
self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 10)
self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 5)
self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3)
except OSError:
pass
elif hasattr(socket, 'TCP_KEEPALIVE'): # macOS
try:
self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPALIVE, 10)
except OSError:
pass
_ver_suffix = WEBUI_VERSION.removeprefix('v')
server_version = ('HermesWebUI/' + _ver_suffix) if _ver_suffix != 'unknown' else 'HermesWebUI'
def log_message(self, fmt, *args): pass # suppress default Apache-style log