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:
1 parent
01f01b9cbe
commit
598fd4ff83
4 files changed
+29
-12
No files matched your search
+4
-1
@@ -435,10 +435,12 @@ def check_auth(handler, parsed) -> bool:
|
|||||||
return True
|
return True
|
||||||
# Not authorized
|
# Not authorized
|
||||||
if parsed.path.startswith('/api/'):
|
if parsed.path.startswith('/api/'):
|
||||||
|
body = b'{"error":"Authentication required"}'
|
||||||
handler.send_response(401)
|
handler.send_response(401)
|
||||||
handler.send_header('Content-Type', 'application/json')
|
handler.send_header('Content-Type', 'application/json')
|
||||||
|
handler.send_header('Content-Length', str(len(body)))
|
||||||
handler.end_headers()
|
handler.end_headers()
|
||||||
handler.wfile.write(b'{"error":"Authentication required"}')
|
handler.wfile.write(body)
|
||||||
else:
|
else:
|
||||||
handler.send_response(302)
|
handler.send_response(302)
|
||||||
# Pass the original path as ?next= so login.js redirects back after auth.
|
# 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.
|
# `?`, `&`, `=`) gets percent-encoded.
|
||||||
_next = _urlparse.quote(_path_with_query, safe='/')
|
_next = _urlparse.quote(_path_with_query, safe='/')
|
||||||
handler.send_header('Location', 'login?next=' + _next)
|
handler.send_header('Location', 'login?next=' + _next)
|
||||||
|
handler.send_header('Content-Length', '0')
|
||||||
handler.end_headers()
|
handler.end_headers()
|
||||||
return False
|
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("Content-Type", "text/event-stream; charset=utf-8")
|
||||||
handler.send_header("Cache-Control", "no-cache")
|
handler.send_header("Cache-Control", "no-cache")
|
||||||
handler.send_header("X-Accel-Buffering", "no")
|
handler.send_header("X-Accel-Buffering", "no")
|
||||||
handler.send_header("Connection", "keep-alive")
|
handler.send_header("Connection", "close")
|
||||||
handler.end_headers()
|
handler.end_headers()
|
||||||
|
|
||||||
# Send an initial frame so the client knows the connection is open
|
# 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)
|
_record_login_attempt(client_ip)
|
||||||
return bad(handler, "Invalid password", 401)
|
return bad(handler, "Invalid password", 401)
|
||||||
cookie_val = create_session()
|
cookie_val = create_session()
|
||||||
|
body = json.dumps({"ok": True}).encode()
|
||||||
handler.send_response(200)
|
handler.send_response(200)
|
||||||
handler.send_header("Content-Type", "application/json")
|
handler.send_header("Content-Type", "application/json")
|
||||||
|
handler.send_header("Content-Length", str(len(body)))
|
||||||
handler.send_header("Cache-Control", "no-store")
|
handler.send_header("Cache-Control", "no-store")
|
||||||
_security_headers(handler)
|
_security_headers(handler)
|
||||||
set_auth_cookie(handler, cookie_val)
|
set_auth_cookie(handler, cookie_val)
|
||||||
handler.end_headers()
|
handler.end_headers()
|
||||||
handler.wfile.write(json.dumps({"ok": True}).encode())
|
handler.wfile.write(body)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if parsed.path == "/api/auth/logout":
|
if parsed.path == "/api/auth/logout":
|
||||||
@@ -6231,13 +6233,15 @@ def handle_post(handler, parsed) -> bool:
|
|||||||
cookie_val = parse_cookie(handler)
|
cookie_val = parse_cookie(handler)
|
||||||
if cookie_val:
|
if cookie_val:
|
||||||
invalidate_session(cookie_val)
|
invalidate_session(cookie_val)
|
||||||
|
body = json.dumps({"ok": True}).encode()
|
||||||
handler.send_response(200)
|
handler.send_response(200)
|
||||||
handler.send_header("Content-Type", "application/json")
|
handler.send_header("Content-Type", "application/json")
|
||||||
|
handler.send_header("Content-Length", str(len(body)))
|
||||||
handler.send_header("Cache-Control", "no-store")
|
handler.send_header("Cache-Control", "no-store")
|
||||||
_security_headers(handler)
|
_security_headers(handler)
|
||||||
clear_auth_cookie(handler)
|
clear_auth_cookie(handler)
|
||||||
handler.end_headers()
|
handler.end_headers()
|
||||||
handler.wfile.write(json.dumps({"ok": True}).encode())
|
handler.wfile.write(body)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# ── Checkpoints / Rollback (POST) ──
|
# ── 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("Content-Type", "text/event-stream; charset=utf-8")
|
||||||
handler.send_header("Cache-Control", "no-cache")
|
handler.send_header("Cache-Control", "no-cache")
|
||||||
handler.send_header("X-Accel-Buffering", "no")
|
handler.send_header("X-Accel-Buffering", "no")
|
||||||
handler.send_header("Connection", "keep-alive")
|
handler.send_header("Connection", "close")
|
||||||
handler.end_headers()
|
handler.end_headers()
|
||||||
try:
|
try:
|
||||||
_replay_run_journal(handler, stream_id, _parse_run_journal_after_seq(qs))
|
_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("Content-Type", "text/event-stream; charset=utf-8")
|
||||||
handler.send_header("Cache-Control", "no-cache")
|
handler.send_header("Cache-Control", "no-cache")
|
||||||
handler.send_header("X-Accel-Buffering", "no")
|
handler.send_header("X-Accel-Buffering", "no")
|
||||||
handler.send_header("Connection", "keep-alive")
|
handler.send_header("Connection", "close")
|
||||||
handler.end_headers()
|
handler.end_headers()
|
||||||
try:
|
try:
|
||||||
while True:
|
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("Content-Type", "text/event-stream; charset=utf-8")
|
||||||
handler.send_header("Cache-Control", "no-cache")
|
handler.send_header("Cache-Control", "no-cache")
|
||||||
handler.send_header("X-Accel-Buffering", "no")
|
handler.send_header("X-Accel-Buffering", "no")
|
||||||
handler.send_header("Connection", "keep-alive")
|
handler.send_header("Connection", "close")
|
||||||
handler.end_headers()
|
handler.end_headers()
|
||||||
try:
|
try:
|
||||||
while True:
|
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('Content-Type', 'text/event-stream; charset=utf-8')
|
||||||
handler.send_header('Cache-Control', 'no-cache')
|
handler.send_header('Cache-Control', 'no-cache')
|
||||||
handler.send_header('X-Accel-Buffering', 'no')
|
handler.send_header('X-Accel-Buffering', 'no')
|
||||||
handler.send_header('Connection', 'keep-alive')
|
handler.send_header('Connection', 'close')
|
||||||
handler.end_headers()
|
handler.end_headers()
|
||||||
|
|
||||||
q = watcher.subscribe()
|
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('Content-Type', 'text/event-stream; charset=utf-8')
|
||||||
handler.send_header('Cache-Control', 'no-cache')
|
handler.send_header('Cache-Control', 'no-cache')
|
||||||
handler.send_header('X-Accel-Buffering', 'no')
|
handler.send_header('X-Accel-Buffering', 'no')
|
||||||
handler.send_header('Connection', 'keep-alive')
|
handler.send_header('Connection', 'close')
|
||||||
handler.end_headers()
|
handler.end_headers()
|
||||||
|
|
||||||
q = subscribe_session_events()
|
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_response(416)
|
||||||
handler.send_header("Content-Range", f"bytes */{file_size}")
|
handler.send_header("Content-Range", f"bytes */{file_size}")
|
||||||
handler.send_header("Accept-Ranges", "bytes")
|
handler.send_header("Accept-Ranges", "bytes")
|
||||||
|
handler.send_header("Content-Length", "0")
|
||||||
_security_headers(handler)
|
_security_headers(handler)
|
||||||
handler.end_headers()
|
handler.end_headers()
|
||||||
return True
|
return True
|
||||||
@@ -7027,10 +7032,12 @@ def _handle_media(handler, parsed):
|
|||||||
if is_auth_enabled():
|
if is_auth_enabled():
|
||||||
cv = parse_cookie(handler)
|
cv = parse_cookie(handler)
|
||||||
if not (cv and verify_session(cv)):
|
if not (cv and verify_session(cv)):
|
||||||
|
body = b'{"error":"Authentication required"}'
|
||||||
handler.send_response(401)
|
handler.send_response(401)
|
||||||
handler.send_header("Content-Type", "application/json")
|
handler.send_header("Content-Type", "application/json")
|
||||||
|
handler.send_header("Content-Length", str(len(body)))
|
||||||
handler.end_headers()
|
handler.end_headers()
|
||||||
handler.wfile.write(b'{"error":"Authentication required"}')
|
handler.wfile.write(body)
|
||||||
return
|
return
|
||||||
|
|
||||||
qs = parse_qs(parsed.query)
|
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('Content-Type', 'text/event-stream; charset=utf-8')
|
||||||
handler.send_header('Cache-Control', 'no-cache')
|
handler.send_header('Cache-Control', 'no-cache')
|
||||||
handler.send_header('X-Accel-Buffering', 'no')
|
handler.send_header('X-Accel-Buffering', 'no')
|
||||||
handler.send_header('Connection', 'keep-alive')
|
handler.send_header('Connection', 'close')
|
||||||
handler.end_headers()
|
handler.end_headers()
|
||||||
|
|
||||||
from api.streaming import _sse
|
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('Content-Type', 'text/event-stream; charset=utf-8')
|
||||||
handler.send_header('Cache-Control', 'no-cache')
|
handler.send_header('Cache-Control', 'no-cache')
|
||||||
handler.send_header('X-Accel-Buffering', 'no')
|
handler.send_header('X-Accel-Buffering', 'no')
|
||||||
handler.send_header('Connection', 'keep-alive')
|
handler.send_header('Connection', 'close')
|
||||||
handler.end_headers()
|
handler.end_headers()
|
||||||
|
|
||||||
from api.streaming import _sse
|
from api.streaming import _sse
|
||||||
|
|||||||
@@ -170,6 +170,13 @@ class QuietHTTPServer(ThreadingHTTPServer):
|
|||||||
|
|
||||||
|
|
||||||
class Handler(BaseHTTPRequestHandler):
|
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
|
timeout = 30 # seconds — kills idle/incomplete connections to prevent thread exhaustion
|
||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
|
|||||||
Reference in new issue
Block a user