{"record":{"id":"e8059ab60d4298db","repo":"NanmiCoder/MediaCrawler","slug":"crawler-is-already-running","errorCode":null,"errorMessage":"Crawler is already running","messagePattern":"Crawler is already running","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"api/routers/crawler.py","lineNumber":34,"sourceCode":"# 详细许可条款请参阅项目根目录下的LICENSE文件。\n# 使用本代码即表示您同意遵守上述原则和LICENSE中的所有条款。\n\nfrom fastapi import APIRouter, HTTPException\n\nfrom ..schemas import CrawlerStartRequest, CrawlerStatusResponse\nfrom ..services import crawler_manager\n\nrouter = APIRouter(prefix=\"/crawler\", tags=[\"crawler\"])\n\n\n@router.post(\"/start\")\nasync def start_crawler(request: CrawlerStartRequest):\n    \"\"\"Start crawler task\"\"\"\n    success = await crawler_manager.start(request)\n    if not success:\n        # Handle concurrent/duplicate requests: if process is already running, return 400 instead of 500\n        if crawler_manager.process and crawler_manager.process.poll() is None:\n            raise HTTPException(status_code=400, detail=\"Crawler is already running\")\n        raise HTTPException(status_code=500, detail=\"Failed to start crawler\")\n\n    return {\"status\": \"ok\", \"message\": \"Crawler started successfully\"}\n\n\n@router.post(\"/stop\")\nasync def stop_crawler():\n    \"\"\"Stop crawler task\"\"\"\n    success = await crawler_manager.stop()\n    if not success:\n        # Handle concurrent/duplicate requests: if process already exited/doesn't exist, return 400 instead of 500\n        if not crawler_manager.process or crawler_manager.process.poll() is not None:\n            raise HTTPException(status_code=400, detail=\"No crawler is running\")\n        raise HTTPException(status_code=500, detail=\"Failed to stop crawler\")\n\n    return {\"status\": \"ok\", \"message\": \"Crawler stopped successfully\"}\n\n","sourceCodeStart":16,"sourceCodeEnd":52,"githubUrl":"https://github.com/NanmiCoder/MediaCrawler/blob/d6f7c5bb906b6dac40ddf343ef9e26438a3de092/api/routers/crawler.py#L16-L52","documentation":"Raised by CDPBrowserManager._get_browser_websocket_url after the Chrome DevTools Protocol /json/version endpoint answered HTTP 200 but the JSON body contained no 'webSocketDebuggerUrl' key. The manager needs that WebSocket endpoint to hand to playwright.chromium.connect_over_cdp. Chrome 136+ intentionally omits/hides the DevTools HTTP endpoints for browsers launched with a persistent user-data-dir, so a 200 response without the WS URL is now a common outcome.","triggerScenarios":"GET http://localhost:{debug_port}/json/version returns 200 with JSON lacking 'webSocketDebuggerUrl' — typically when connecting to an existing Chrome 136+ instance started with --remote-debugging-port on a non-empty user profile, or when another client already holds the DevTools socket.","commonSituations":"Upgrading Chrome past v136 while keeping the 'attach to my daily browser' workflow; launching Chrome with --user-data-dir pointing at the default profile; anti-debugging enterprise Chrome builds; connecting to a browser whose DevTools server was started with --remote-allow-origins restrictions.","solutions":["If targeting an existing browser, enable the config.CDP_CONNECT_EXISTING path (see _connect_via_cdp) which skips /json/version and calls connect_over_cdp with the http endpoint directly","Launch the debug Chrome with a dedicated empty user-data-dir (e.g. --user-data-dir=/tmp/chrome-debug) so /json/version exposes webSocketDebuggerUrl again","Retry the fetch for a few seconds after browser start — the endpoint can briefly return a stub body while DevTools initializes","If you must use the daily profile on Chrome 136+, pass the CDP endpoint URL (http://localhost:{port}) straight to playwright.chromium.connect_over_cdp instead of parsing /json/version"],"exampleFix":"// before\nws_url = data.get(\"webSocketDebuggerUrl\")\nif ws_url:\n    return ws_url\nelse:\n    raise RuntimeError(\"webSocketDebuggerUrl not found\")\n\n// after\nws_url = data.get(\"webSocketDebuggerUrl\")\nif ws_url:\n    return ws_url\n# Chrome 136+ on existing profiles: fall back to the raw HTTP endpoint,\n# which connect_over_cdp accepts directly\nreturn f\"http://localhost:{debug_port}\"","handlingStrategy":"fallback","validationCode":"async def get_ws_url_or_endpoint(debug_port: int) -> str:\n    async with make_async_client(follow_redirects=True) as client:\n        resp = await client.get(f\"http://localhost:{debug_port}/json/version\", timeout=10)\n    if resp.status_code == 200:\n        ws = resp.json().get(\"webSocketDebuggerUrl\")\n        if ws:\n            return ws\n    # Chrome 136+ / existing profile: connect_over_cdp accepts the HTTP endpoint\n    return f\"http://localhost:{debug_port}\"","typeGuard":"def has_ws_url(data: dict) -> bool:\n    return isinstance(data, dict) and isinstance(data.get(\"webSocketDebuggerUrl\"), str) and data[\"webSocketDebuggerUrl\"].startswith((\"ws://\", \"wss://\"))","tryCatchPattern":"try:\n    ws_url = await manager._get_browser_websocket_url(port)\nexcept RuntimeError:\n    ws_url = f\"http://localhost:{port}\"  # Chrome 136+ fallback\n# caller must still validate connection afterwards:\nassert manager.browser and manager.browser.is_connected()","preventionTips":["Pin the debug Chrome to a dedicated --user-data-dir so /json/version keeps exposing webSocketDebuggerUrl","Detect Chrome major version at startup and route existing-browser flows through the CDP_CONNECT_EXISTING branch","Never cache ws URLs across sessions — always re-fetch /json/version right before connecting"],"tags":["cdp","chrome","playwright","websocket","devtools","browser"],"backgroundTag":null,"analyzedSha":"d6f7c5bb906b6dac40ddf343ef9e26438a3de092","analyzedAt":"2026-08-15T01:39:07.505Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}