Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,12 @@ The test suite covers:
- If your setup uses a different route, set `CHROME_HOST_IN_CONTAINER` explicitly before launch
- Re-run `./launch.bash doctor` and check the logged "Effective CHROME_HOST_IN_CONTAINER"

### WebSocket connection fails with "Handshake status 500" or "Host header" error

Recent versions of Google Chrome strictly enforce security checks on the `Host` header for DevTools connections. They reject hostnames that are not `localhost` or a raw IP address.

WebVisionKit handles this by automatically resolving `host.docker.internal` to its numerical IP address inside the container before connecting. This satisfies Chrome's security policy while maintaining the network route from Docker to the host. If you still encounter this error, ensure that `host.docker.internal` is correctly defined in your container's `/etc/hosts` (standard Docker behavior).

### Output files are owned by root

On native Linux and WSL, WebVisionKit runs the container with the calling user’s UID and GID by default so output files stay user-owned. If you override container args manually, avoid removing that behavior unless you want root-owned artifacts.
16 changes: 15 additions & 1 deletion api/webvisionkit/cdp.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,28 @@
from .errors import RecoverableStreamError


import socket
from urllib.parse import urlparse

class CDPClient:
def __init__(self, ws_url: str, receive_timeout_seconds: float) -> None:
if websocket is None:
raise RecoverableStreamError("websocket-client is not installed inside the container.")

self.ws_url = ws_url
# Bypassing Chrome's Host header check by using an IP address instead of 'host.docker.internal'
# Chrome DevTools security (anti-DNS-rebinding) rejects hostnames that are not 'localhost' or an IP.
# Since 'host.docker.internal' is a name, we resolve it to an IP inside the container.
parsed = urlparse(ws_url)
if parsed.hostname == "host.docker.internal":

Check failure on line 30 in api/webvisionkit/cdp.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "host.docker.internal" 3 times.

See more on https://sonarcloud.io/project/issues?id=PXLAIRobotics_WebVisionKitFramework&issues=AZ4dhaVBScxkJksmDPp6&open=AZ4dhaVBScxkJksmDPp6&pullRequest=1
try:
ip = socket.gethostbyname("host.docker.internal")
self.ws_url = ws_url.replace("host.docker.internal", ip, 1)
except Exception:
pass

self.receive_timeout_seconds = receive_timeout_seconds
self.ws = websocket.create_connection(ws_url, timeout=10)
self.ws = websocket.create_connection(self.ws_url, timeout=10)
self.ws.settimeout(receive_timeout_seconds)
self.next_id = 1
self.pending_messages: Deque[Dict[str, Any]] = deque()
Expand Down
2 changes: 1 addition & 1 deletion launch.bash
Original file line number Diff line number Diff line change
Expand Up @@ -1763,7 +1763,7 @@ run_container_devtools_probe() {

browser_browser_ws_url_host="$(resolve_browser_ws_url)"
browser_browser_ws_url_for_container="$(rewrite_ws_url_for_container "${browser_browser_ws_url_host}")"
probe_code=$'import json\nimport os\n\nimport websocket\n\nws_url = os.environ["BROWSER_BROWSER_WS_URL"]\nws = websocket.create_connection(ws_url, timeout=5)\ntry:\n ws.send(json.dumps({"id": 1, "method": "Target.getTargets", "params": {}}))\n payload = json.loads(ws.recv())\nfinally:\n ws.close()\nif "result" not in payload:\n raise RuntimeError(f"Browser websocket probe failed: {payload}")\ntarget_infos = payload["result"].get("targetInfos", [])\nprint(ws_url)\nprint(f"targets={len(target_infos)}")\n'
probe_code=$'import json\nimport os\nimport socket\n\nimport websocket\n\nws_url = os.environ["BROWSER_BROWSER_WS_URL"]\n# Resolve host.docker.internal to an IP to bypass Chrome\'s Host header security check\n# (anti-DNS-rebinding) which rejects hostnames that are not localhost or an IP.\nif "host.docker.internal" in ws_url:\n try:\n ip = socket.gethostbyname("host.docker.internal")\n ws_url = ws_url.replace("host.docker.internal", ip, 1)\n except:\n pass\nws = websocket.create_connection(ws_url, timeout=5)\ntry:\n ws.send(json.dumps({"id": 1, "method": "Target.getTargets", "params": {}}))\n payload = json.loads(ws.recv())\nfinally:\n ws.close()\nif "result" not in payload:\n raise RuntimeError(f"Browser websocket probe failed: {payload}")\ntarget_infos = payload["result"].get("targetInfos", [])\nprint(ws_url)\nprint(f"targets={len(target_infos)}")\n'

docker run \
"${docker_args[@]}" \
Expand Down