grafana/k6 · error
sending a message to browser: %w
Error message
sending a message to browser: %w
What it means
Connection.send publishes a CDP message and simultaneously watches errorCh; if the sendLoop reports a websocket write error (broken/aborted connection), send fails with this wrapper. It means the command never (reliably) reached the browser — the transport was already broken when k6 tried to write.
Source
Thrown at internal/js/modules/k6/browser/common/connection.go:480
msg := &cdproto.Message{
ID: c.msgIDGen.newID(),
SessionID: sid,
Method: cdproto.MethodType(cdpruntime.CommandRunIfWaitingForDebugger),
}
err := c.send(c.ctx, msg, nil, nil)
if err != nil {
c.logger.Errorf("Connection:stopWaitingForDebugger", "sid:%v wsURL:%q, err:%v", sid, c.wsURL, err)
}
}
func (c *Connection) send(
ctx context.Context, msg *cdproto.Message, recvCh chan *cdproto.Message, res any,
) error {
select {
case c.sendCh <- msg:
case err := <-c.errorCh:
c.logger.Debugf("Connection:send:<-c.errorCh", "wsURL:%q sid:%v, err:%v", c.wsURL, msg.SessionID, err)
return fmt.Errorf("sending a message to browser: %w", err)
case code := <-c.closeCh:
c.logger.Debugf("Connection:send:<-c.closeCh", "wsURL:%q sid:%v, websocket code:%v", c.wsURL, msg.SessionID, code)
_ = c.close(code)
return fmt.Errorf("closing communication with browser: %w", &websocket.CloseError{Code: code})
case <-ctx.Done():
c.logger.Debugf("Connection:send:<-ctx.Done", "wsURL:%q sid:%v err:%v", c.wsURL, msg.SessionID, ContextErr(ctx))
return nil
case <-c.done:
c.logger.Debugf("Connection:send:<-c.done", "wsURL:%q sid:%v", c.wsURL, msg.SessionID)
return nil
}
// Block waiting for response.
if recvCh == nil {
return nil
}
tid := c.findTargetIDForLog(msg.SessionID)
select {View on GitHub (pinned to 93accf6570)
Solutions
- Check whether the browser process is still alive and consult its logs for the crash reason.
- For remote browsers, keepalive/tune LB idle timeouts so ws connections are not reaped mid-test.
- Make scripts resilient: catch the error, re-acquire page/context, and retry the logical action once.
- If it recurs at a specific step, capture K6_BROWSER_ENABLE_DEBUGGING output to see which CDP domain call breaks.
Example fix
// before
await page.goto('https://example.com'); // throws 'sending a message to browser: ...' if ws broke
// after
async function gotoRetry(page, url, tries = 2) {
for (let i = 0; i < tries; i++) {
try { return await page.goto(url); }
catch (e) {
if (i === tries - 1) throw e;
await sleep(1000);
}
}
} Defensive patterns
Strategy: try-catch
Try / catch
try {
await page.goto(url, { waitUntil: 'load' });
} catch (e) {
if (/sending a message to browser/.test(String(e))) {
// transport was broken: re-open a page (or context) and retry the logical step once
const p2 = await browser.newPage();
await p2.goto(url, { waitUntil: 'load' });
} else {
throw e;
}
} Prevention
- Don't reuse a page after any error mentioning the connection: re-create page/context first.
- For remote browsers, keep connections warm or lower LB idle timeouts' impact with periodic activity.
- Cap action timeouts so a dead connection fails fast instead of hanging sends.
When it happens
Trigger: Any CDP operation (page.goto, click, evaluate, screenshot…) issued after or exactly while the websocket breaks: browser crashed mid-test, connection reset by the remote browser, or network interruption to a remote browser.
Common situations: Browser OOM-killed mid-scenario; remote browser pod evicted; long-running tests where an idle connection is reaped by a load balancer; operations attempted after a prior unnoticed close.
Related errors
- connecting to Chromium over CDP: %w
- WebSocket endpoint cannot be empty
- lost connection while closing the browser (websocket url: %s
- connection closed with websocket code: %d
- closing communication with browser: %w
AI-assisted analysis of grafana/k6@93accf6570 (2026-08-15).
Data as JSON: /api/errors/8b40f9788ba67d86.
Report an issue: GitHub.