{"record":{"id":"e3f552e7fc0b9c89","repo":"grafana/k6","slug":"connecting-to-chromium-over-cdp-w","errorCode":null,"errorMessage":"connecting to Chromium over CDP: %w","messagePattern":"connecting to Chromium over CDP: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/js/modules/k6/browser/browser/chromium_mapping.go","lineNumber":28,"sourceCode":")\n\n// mapChromium maps the Chromium browser type API to the JS module.\nfunc mapChromium(vu moduleVU, bt *chromium.BrowserType) mapping {\n\treturn mapping{\n\t\t\"connectOverCDP\": func(wsEndpoint string) *sobek.Promise {\n\t\t\treturn promise(vu, func() (any, error) {\n\t\t\t\titer := vu.State().Iteration\n\n\t\t\t\t// Clone the BrowserType for this call so concurrent\n\t\t\t\t// connectOverCDP calls in the same iteration (e.g., via\n\t\t\t\t// Promise.all) don't race on its mutable state.\n\t\t\t\tconnBT := bt.Clone()\n\n\t\t\t\t// Link the connection to the iteration trace and connect.\n\t\t\t\ttracedCtx := vu.startConnectTrace(vu.Context(), iter)\n\t\t\t\tb, err := connBT.ConnectOverCDP(tracedCtx, wsEndpoint)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"connecting to Chromium over CDP: %w\", err)\n\t\t\t\t}\n\n\t\t\t\t// Register for guaranteed cleanup at IterEnd / Exit.\n\t\t\t\tvu.trackUserManagedBrowser(iter, b)\n\n\t\t\t\treturn mapBrowser(vu, func() (*common.Browser, error) {\n\t\t\t\t\treturn b, nil\n\t\t\t\t}), nil\n\t\t\t})\n\t\t},\n\t}\n}\n","sourceCodeStart":10,"sourceCodeEnd":41,"githubUrl":"https://github.com/grafana/k6/blob/93accf6570dcd306ca5e99cc44c393ee3797761b/internal/js/modules/k6/browser/browser/chromium_mapping.go#L10-L41","documentation":"browser.chromium.connectOverCDP(wsEndpoint) attaches k6 to a user-managed Chromium over the DevTools Protocol: it validates the ws:// URL up front, initializes browser options from K6_BROWSER_* env vars, then dials the endpoint; any dial/handshake failure is wrapped as 'connecting to Chromium over CDP' (with a UserFriendlyError that surfaces timeout context). It exists so a browser scenario is not required - you bring your own Chromium and pass its WebSocket debugger URL.","triggerScenarios":"wsEndpoint empty, not a ws:// URL, or malformed (caught by validateWSEndpoint); passing an http:// endpoint where a ws:// webSocketDebuggerUrl is required; Chromium not started with --remote-debugging-port; wrong host/port; debugger reachable but the browser exits or the WebSocket handshake times out (K6_BROWSER_TIMEOUT).","commonSituations":"Connecting k6 to containerized or remote Chromium without publishing the debug port; grabbing the URL from the wrong /json endpoint entry (page target instead of browser target); Chrome security restrictions on remote debugging with non-127.0.0.1 addresses; firewalled CI runners.","solutions":["Start Chromium with remote debugging: chromium --headless --remote-debugging-port=9222 --no-sandbox (as needed)","Fetch the browser-level WebSocket URL from http://127.0.0.1:9222/json/version and pass its webSocketDebuggerUrl (ws://...) to connectOverCDP","Verify reachability first: curl http://HOST:9222/json/version from the same host/network k6 runs on","If the handshake times out, raise K6_BROWSER_TIMEOUT and confirm the browser process stays alive for the whole iteration"],"exampleFix":"// before: http endpoint / guessed ws URL\nimport browser from 'k6/browser';\nexport default async () => {\n  const b = browser.chromium.connectOverCDP('http://127.0.0.1:9222'); // wrong scheme\n};\n\n// after: resolve the real browser ws endpoint, then connect\nimport http from 'k6/http';\nimport browser from 'k6/browser';\nconst ver = http.get('http://127.0.0.1:9222/json/version').json();\nexport default async () => {\n  const b = browser.chromium.connectOverCDP(ver.webSocketDebuggerUrl);\n  const page = await b.newPage();\n  await page.goto('https://k6.io');\n};","handlingStrategy":"validation","validationCode":"// verify the debugger is reachable and resolve the browser ws URL before connecting\nimport http from 'k6/http';\nconst res = http.get('http://127.0.0.1:9222/json/version');\nif (res.status !== 200) throw new Error(`CDP endpoint unreachable (HTTP ${res.status})`);\nconst ws = res.json().webSocketDebuggerUrl;\nif (typeof ws !== 'string' || !ws.startsWith('ws://')) throw new Error('no valid webSocketDebuggerUrl');\n// pass `ws` to browser.chromium.connectOverCDP inside the default function","typeGuard":"function isWsEndpoint(v) {\n  return typeof v === 'string' && /^wss?:\\/\\/[^\\s/]+(\\/\\S*)?$/.test(v);\n}","tryCatchPattern":"try {\n  const b = browser.chromium.connectOverCDP(ws);\n} catch (e) {\n  const msg = String(e);\n  if (msg.includes('WebSocket endpoint')) throw new Error('fix the ws:// URL: ' + msg);\n  if (msg.includes('timed out') || msg.includes('timeout')) throw new Error('browser not reachable - check port and K6_BROWSER_TIMEOUT');\n  if (msg.includes('connecting to Chromium over CDP')) throw new Error('handshake failed - is Chromium running with --remote-debugging-port?');\n  throw e;\n}","preventionTips":["Always derive wsEndpoint from /json/version's webSocketDebuggerUrl instead of hand-typing it","Start Chromium with --remote-debugging-port and keep the process alive for the whole test","From the k6 host, curl the /json/version endpoint first - if curl cannot reach it, k6 cannot either","Use ws:// (or wss://) - an http:// URL is not a WebSocket endpoint"],"tags":["browser","cdp","chromium","websocket","connection"],"backgroundTag":null,"analyzedSha":"93accf6570dcd306ca5e99cc44c393ee3797761b","analyzedAt":"2026-08-15T21:23:27.118Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}