fix(web): raise uvicorn WS frame cap for Desktop file.attach

Uvicorn's 16 MiB default drops one-shot base64 remote attachments before
Hermes sees them. Raise ws_max_size to fit the 256 MiB attach reader after
base64 expansion, and bump DESKTOP_BACKEND_CONTRACT to v5 so older remotes
surface skew instead of silent disconnects.

Co-authored-by: Börje <borje@dqsverige.se>
This commit is contained in:
Brooklyn Nicholson 2026-07-28 19:20:14 -05:00
parent 9dc191d437
commit 612b23f6c0
3 changed files with 24 additions and 1 deletions

View file

@ -318,6 +318,13 @@ def _apply_ssh_owner_nonce(nonce: Optional[str]) -> None:
# injection share a single, testable seam.
_DASHBOARD_EMBEDDED_CHAT_ENABLED = True
# Desktop's file.attach compatibility transport sends a complete base64 data
# URL in one JSON-RPC frame. Uvicorn defaults to 16 MiB, which rejects files at
# the preview ceiling before the dispatcher sees them. Keep the gateway
# finite while allowing the 256 MiB raw Desktop attach cap plus base64/JSON
# overhead.
_DESKTOP_ATTACHMENT_WS_MAX_BYTES = 384 * 1024 * 1024
# Simple rate limiter for the reveal endpoint
_reveal_timestamps: List[float] = []
_REVEAL_MAX_PER_WINDOW = 5
@ -20290,6 +20297,7 @@ def start_server(
# reaped via the WebSocketDisconnect → disconnect/reap path.
ws_ping_interval=None if _is_loopback else 20.0,
ws_ping_timeout=None if _is_loopback else 20.0,
ws_max_size=_DESKTOP_ATTACHMENT_WS_MAX_BYTES,
)
server = uvicorn.Server(config)

View file

@ -107,6 +107,20 @@ def test_start_server_disables_ws_ping_on_loopback(monkeypatch):
assert captured["ws_ping_timeout"] is None
def test_start_server_accepts_base64_desktop_attachments_above_preview_limit(monkeypatch):
"""The gateway frame cap must fit the Desktop attachment default after
base64 expansion and JSON framing; uvicorn's 16 MiB default would reject
the request before ``file.attach`` can stage it.
"""
captured = _stub_uvicorn(monkeypatch)
web_server.start_server(host="127.0.0.1", port=0, open_browser=False)
raw_attachment_bytes = 256 * 1024 * 1024
base64_bytes = ((raw_attachment_bytes + 2) // 3) * 4
assert captured["ws_max_size"] > base64_bytes
def test_start_server_enables_ws_ping_for_half_open_detection(monkeypatch):
"""Non-loopback (public) binds MUST keep the ws ping enabled so half-open
connections (reverse-proxy 524, dropped Cloudflare Tunnel) raise

View file

@ -4528,7 +4528,8 @@ def _current_profile_name() -> str:
# v2: adds the file.attach RPC (remote-gateway non-image file upload).
# v3: adds approvals.mode config RPCs and session.info reconciliation.
# v4: session.create fast=false is an explicit per-session normal-tier override.
DESKTOP_BACKEND_CONTRACT = 4
# v5: uvicorn ws_max_size raised for one-shot base64 file.attach frames (>16 MiB).
DESKTOP_BACKEND_CONTRACT = 5
def _session_usage_snapshot(session: dict | None) -> dict: