SeleniumHQ/selenium · error · Error

BiDi connection is not open

Error message

BiDi connection is not open

What it means

Thrown by `BiDi.send()` as a defense-in-depth check: after `waitForConnection()` resolves, the method re-checks `this._ws.readyState !== WebSocket.OPEN`. This catches the race where the socket transitioned to CLOSING/CLOSED between the handshake completing and the send (e.g. the caller closed the raw socket, or the peer dropped immediately after opening). Unlike [38], this can fire even though `_closed` is not yet set, because the close/error handler may not have run yet.

Source

Thrown at javascript/selenium-webdriver/bidi/index.js:214

  }

  /**
   * Sends a bidi request
   * @param params
   * @returns {Promise<unknown>}
   */
  async send(params) {
    if (this._closed) {
      throw new Error('BiDi connection is closed')
    }
    if (!this.connected) {
      await this.waitForConnection()
    }
    // Defense in depth: even after waitForConnection() resolves, the socket
    // may have transitioned to CLOSING/CLOSED (e.g. caller closed the raw
    // socket). Refuse rather than throwing from inside ws.send().
    if (this._ws.readyState !== WebSocket.OPEN) {
      throw new Error('BiDi connection is not open')
    }

    const id = ++this.id

    this._ws.send(JSON.stringify({ id, ...params }))

    return new Promise((resolve, reject) => {
      const timeoutId = setTimeout(() => {
        this._pending.delete(id)
        reject(new Error(`Request with id ${id} timed out`))
      }, RESPONSE_TIMEOUT)

      this._pending.set(id, { resolve, reject, timeoutId })
    })
  }

  /**
   * Subscribe to events

View on GitHub (pinned to aa36b38e69)

Solutions

  1. Avoid closing the raw socket (`bidi.socket`) directly — use `bidi.close()` which coordinates teardown
  2. Serialize teardown and BiDi commands so close() cannot race an in-flight send()
  3. Retry the command after re-acquiring the connection if the session is still alive

Example fix

// before — concurrent raw close races the send
bidi.socket.close()       // raw close
await bidi.send(cmd)      // may throw: connection is not open

// after — coordinated close
await bidi.close()        // fails pending sends cleanly
Defensive patterns

Strategy: try-catch

Validate before calling

if (bidi.socket && bidi.socket.readyState === 1 /* OPEN */) {
  await bidi.send(cmd)
}

Type guard

const isSocketOpen = (bidi) => bidi.socket?.readyState === 1

Try / catch

try {
  await bidi.send(cmd)
} catch (e) {
  if (/connection is not open/i.test(e.message)) {
    // transient race — reconnect via driver.getBidi() then retry once
    return
  }
  throw e
}

Prevention

When it happens

Trigger: A concurrent caller closes the raw socket (`bidi.socket.close()`) while another send() is racing through waitForConnection(); the peer server closes the WebSocket immediately after the handshake; tight interleaving between close() and send().

Common situations: Concurrent BiDi traffic during teardown; an interceptor's handler closes the socket while another command is in-flight; server-side connection limits dropping the socket right after open.

Related errors


AI-assisted analysis of SeleniumHQ/selenium@aa36b38e69 (2026-08-14). Data as JSON: /api/errors/b77398d2253db8d7. Report an issue: GitHub.