mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-30 21:50:16 +00:00
perf(http): enable HTTP/1.1 keep-alive
Enable HTTP/1.1 on the WebUI server so browsers can reuse TCP
connections across normal API/static requests. Tighten response framing
by adding Content-Length to short manual responses and marking
SSE/streaming responses as Connection: close, keeping HTTP/1.1 message
boundaries unambiguous.
Verified:
- python3 -m py_compile server.py api/auth.py api/routes.py api/kanban_bridge.py
- pytest tests/test_auth_*.py tests/test_*sse*.py tests/test_pr1350_*.py
tests/test_pr1355_sse_handler_no_deadlock.py tests/test_kanban_bridge.py
tests/test_logs_ui_static.py tests/test_onboarding_static.py
tests/test_regressions.py tests/test_1038_pwa_auth_redirect.py
tests/test_issue1623_sse_heartbeat_alignment.py
→ 239 passed, 1 skipped
This commit is contained in:
+4
-1
@@ -435,10 +435,12 @@ def check_auth(handler, parsed) -> bool:
|
||||
return True
|
||||
# Not authorized
|
||||
if parsed.path.startswith('/api/'):
|
||||
body = b'{"error":"Authentication required"}'
|
||||
handler.send_response(401)
|
||||
handler.send_header('Content-Type', 'application/json')
|
||||
handler.send_header('Content-Length', str(len(body)))
|
||||
handler.end_headers()
|
||||
handler.wfile.write(b'{"error":"Authentication required"}')
|
||||
handler.wfile.write(body)
|
||||
else:
|
||||
handler.send_response(302)
|
||||
# Pass the original path as ?next= so login.js redirects back after auth.
|
||||
@@ -468,6 +470,7 @@ def check_auth(handler, parsed) -> bool:
|
||||
# `?`, `&`, `=`) gets percent-encoded.
|
||||
_next = _urlparse.quote(_path_with_query, safe='/')
|
||||
handler.send_header('Location', 'login?next=' + _next)
|
||||
handler.send_header('Content-Length', '0')
|
||||
handler.end_headers()
|
||||
return False
|
||||
|
||||
|
||||
@@ -1022,7 +1022,7 @@ def _handle_events_sse_stream(handler, parsed):
|
||||
handler.send_header("Content-Type", "text/event-stream; charset=utf-8")
|
||||
handler.send_header("Cache-Control", "no-cache")
|
||||
handler.send_header("X-Accel-Buffering", "no")
|
||||
handler.send_header("Connection", "keep-alive")
|
||||
handler.send_header("Connection", "close")
|
||||
handler.end_headers()
|
||||
|
||||
# Send an initial frame so the client knows the connection is open
|
||||
|
||||
+17
-10
@@ -6216,13 +6216,15 @@ def handle_post(handler, parsed) -> bool:
|
||||
_record_login_attempt(client_ip)
|
||||
return bad(handler, "Invalid password", 401)
|
||||
cookie_val = create_session()
|
||||
body = json.dumps({"ok": True}).encode()
|
||||
handler.send_response(200)
|
||||
handler.send_header("Content-Type", "application/json")
|
||||
handler.send_header("Content-Length", str(len(body)))
|
||||
handler.send_header("Cache-Control", "no-store")
|
||||
_security_headers(handler)
|
||||
set_auth_cookie(handler, cookie_val)
|
||||
handler.end_headers()
|
||||
handler.wfile.write(json.dumps({"ok": True}).encode())
|
||||
handler.wfile.write(body)
|
||||
return True
|
||||
|
||||
if parsed.path == "/api/auth/logout":
|
||||
@@ -6231,13 +6233,15 @@ def handle_post(handler, parsed) -> bool:
|
||||
cookie_val = parse_cookie(handler)
|
||||
if cookie_val:
|
||||
invalidate_session(cookie_val)
|
||||
body = json.dumps({"ok": True}).encode()
|
||||
handler.send_response(200)
|
||||
handler.send_header("Content-Type", "application/json")
|
||||
handler.send_header("Content-Length", str(len(body)))
|
||||
handler.send_header("Cache-Control", "no-store")
|
||||
_security_headers(handler)
|
||||
clear_auth_cookie(handler)
|
||||
handler.end_headers()
|
||||
handler.wfile.write(json.dumps({"ok": True}).encode())
|
||||
handler.wfile.write(body)
|
||||
return True
|
||||
|
||||
# ── Checkpoints / Rollback (POST) ──
|
||||
@@ -6577,7 +6581,7 @@ def _handle_sse_stream(handler, parsed):
|
||||
handler.send_header("Content-Type", "text/event-stream; charset=utf-8")
|
||||
handler.send_header("Cache-Control", "no-cache")
|
||||
handler.send_header("X-Accel-Buffering", "no")
|
||||
handler.send_header("Connection", "keep-alive")
|
||||
handler.send_header("Connection", "close")
|
||||
handler.end_headers()
|
||||
try:
|
||||
_replay_run_journal(handler, stream_id, _parse_run_journal_after_seq(qs))
|
||||
@@ -6589,7 +6593,7 @@ def _handle_sse_stream(handler, parsed):
|
||||
handler.send_header("Content-Type", "text/event-stream; charset=utf-8")
|
||||
handler.send_header("Cache-Control", "no-cache")
|
||||
handler.send_header("X-Accel-Buffering", "no")
|
||||
handler.send_header("Connection", "keep-alive")
|
||||
handler.send_header("Connection", "close")
|
||||
handler.end_headers()
|
||||
try:
|
||||
while True:
|
||||
@@ -6720,7 +6724,7 @@ def _handle_terminal_output(handler, parsed):
|
||||
handler.send_header("Content-Type", "text/event-stream; charset=utf-8")
|
||||
handler.send_header("Cache-Control", "no-cache")
|
||||
handler.send_header("X-Accel-Buffering", "no")
|
||||
handler.send_header("Connection", "keep-alive")
|
||||
handler.send_header("Connection", "close")
|
||||
handler.end_headers()
|
||||
try:
|
||||
while True:
|
||||
@@ -6798,7 +6802,7 @@ def _handle_gateway_sse_stream(handler, parsed):
|
||||
handler.send_header('Content-Type', 'text/event-stream; charset=utf-8')
|
||||
handler.send_header('Cache-Control', 'no-cache')
|
||||
handler.send_header('X-Accel-Buffering', 'no')
|
||||
handler.send_header('Connection', 'keep-alive')
|
||||
handler.send_header('Connection', 'close')
|
||||
handler.end_headers()
|
||||
|
||||
q = watcher.subscribe()
|
||||
@@ -6831,7 +6835,7 @@ def _handle_session_events_stream(handler):
|
||||
handler.send_header('Content-Type', 'text/event-stream; charset=utf-8')
|
||||
handler.send_header('Cache-Control', 'no-cache')
|
||||
handler.send_header('X-Accel-Buffering', 'no')
|
||||
handler.send_header('Connection', 'keep-alive')
|
||||
handler.send_header('Connection', 'close')
|
||||
handler.end_headers()
|
||||
|
||||
q = subscribe_session_events()
|
||||
@@ -6917,6 +6921,7 @@ def _serve_file_bytes(handler, target: Path, mime: str, disposition: str, cache_
|
||||
handler.send_response(416)
|
||||
handler.send_header("Content-Range", f"bytes */{file_size}")
|
||||
handler.send_header("Accept-Ranges", "bytes")
|
||||
handler.send_header("Content-Length", "0")
|
||||
_security_headers(handler)
|
||||
handler.end_headers()
|
||||
return True
|
||||
@@ -7027,10 +7032,12 @@ def _handle_media(handler, parsed):
|
||||
if is_auth_enabled():
|
||||
cv = parse_cookie(handler)
|
||||
if not (cv and verify_session(cv)):
|
||||
body = b'{"error":"Authentication required"}'
|
||||
handler.send_response(401)
|
||||
handler.send_header("Content-Type", "application/json")
|
||||
handler.send_header("Content-Length", str(len(body)))
|
||||
handler.end_headers()
|
||||
handler.wfile.write(b'{"error":"Authentication required"}')
|
||||
handler.wfile.write(body)
|
||||
return
|
||||
|
||||
qs = parse_qs(parsed.query)
|
||||
@@ -7389,7 +7396,7 @@ def _handle_approval_sse_stream(handler, parsed):
|
||||
handler.send_header('Content-Type', 'text/event-stream; charset=utf-8')
|
||||
handler.send_header('Cache-Control', 'no-cache')
|
||||
handler.send_header('X-Accel-Buffering', 'no')
|
||||
handler.send_header('Connection', 'keep-alive')
|
||||
handler.send_header('Connection', 'close')
|
||||
handler.end_headers()
|
||||
|
||||
from api.streaming import _sse
|
||||
@@ -7490,7 +7497,7 @@ def _handle_clarify_sse_stream(handler, parsed):
|
||||
handler.send_header('Content-Type', 'text/event-stream; charset=utf-8')
|
||||
handler.send_header('Cache-Control', 'no-cache')
|
||||
handler.send_header('X-Accel-Buffering', 'no')
|
||||
handler.send_header('Connection', 'keep-alive')
|
||||
handler.send_header('Connection', 'close')
|
||||
handler.end_headers()
|
||||
|
||||
from api.streaming import _sse
|
||||
|
||||
@@ -170,6 +170,13 @@ class QuietHTTPServer(ThreadingHTTPServer):
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
# HTTP/1.1 enables keep-alive connection reuse — major latency win on
|
||||
# high-RTT links where every saved TCP handshake is 2×RTT. Each response
|
||||
# MUST declare framing (Content-Length, Transfer-Encoding: chunked, or
|
||||
# Connection: close) so the client knows where the message ends. Helpers
|
||||
# j()/t() emit Content-Length; SSE/streaming endpoints emit
|
||||
# Connection: close because the body has no terminator. See PR notes.
|
||||
protocol_version = "HTTP/1.1"
|
||||
timeout = 30 # seconds — kills idle/incomplete connections to prevent thread exhaustion
|
||||
|
||||
def setup(self):
|
||||
|
||||
Reference in New Issue
Block a user