"""HTTP client for Browser Worker VPS.""" from __future__ import annotations import logging from typing import Any from urllib.parse import urlparse, urlunparse import httpx from server.config import settings logger = logging.getLogger("nexus.browser.worker") class BrowserWorkerError(Exception): def __init__(self, message: str, status_code: int = 503) -> None: super().__init__(message) self.status_code = status_code def browser_enabled() -> bool: return ( settings.BROWSER_ENABLED.lower() in ("1", "true", "yes") and bool(settings.BROWSER_WORKER_URL) and bool(settings.BROWSER_WORKER_SECRET) ) def _headers() -> dict[str, str]: return {"X-Nexus-Worker-Key": settings.BROWSER_WORKER_SECRET} def worker_stream_url(session_id: str) -> str: base = settings.BROWSER_WORKER_URL.rstrip("/") parsed = urlparse(base) scheme = "wss" if parsed.scheme == "https" else "ws" netloc = parsed.netloc or parsed.path path = f"/v1/sessions/{session_id}/stream" return urlunparse((scheme, netloc, path, "", "", "")) async def create_worker_session(session_id: str, viewport_w: int, viewport_h: int) -> dict[str, Any]: if not browser_enabled(): raise BrowserWorkerError("Browser worker not enabled", 503) url = f"{settings.BROWSER_WORKER_URL.rstrip('/')}/v1/sessions" payload = {"session_id": session_id, "viewport_w": viewport_w, "viewport_h": viewport_h} try: async with httpx.AsyncClient(timeout=30.0, verify=False) as client: resp = await client.post(url, json=payload, headers=_headers()) except httpx.RequestError as exc: logger.warning("worker create failed: %s", exc) raise BrowserWorkerError("Browser worker unreachable", 503) from exc if resp.status_code >= 400: detail = resp.text[:200] raise BrowserWorkerError(detail or "Worker rejected session", resp.status_code) return resp.json() async def delete_worker_session(session_id: str) -> None: if not browser_enabled(): return url = f"{settings.BROWSER_WORKER_URL.rstrip('/')}/v1/sessions/{session_id}" try: async with httpx.AsyncClient(timeout=15.0, verify=False) as client: await client.delete(url, headers=_headers()) except httpx.RequestError as exc: logger.warning("worker delete failed session=%s: %s", session_id, exc) async def worker_health() -> dict[str, Any] | None: if not browser_enabled(): return None url = f"{settings.BROWSER_WORKER_URL.rstrip('/')}/health" try: async with httpx.AsyncClient(timeout=5.0, verify=False) as client: resp = await client.get(url) if resp.status_code == 200: return resp.json() except httpx.RequestError: return None return None