{"record":{"id":"b77398d2253db8d7","repo":"SeleniumHQ/selenium","slug":"bidi-connection-is-not-open","errorCode":null,"errorMessage":"BiDi connection is not open","messagePattern":"BiDi connection is not open","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"javascript/selenium-webdriver/bidi/index.js","lineNumber":214,"sourceCode":"  }\n\n  /**\n   * Sends a bidi request\n   * @param params\n   * @returns {Promise<unknown>}\n   */\n  async send(params) {\n    if (this._closed) {\n      throw new Error('BiDi connection is closed')\n    }\n    if (!this.connected) {\n      await this.waitForConnection()\n    }\n    // Defense in depth: even after waitForConnection() resolves, the socket\n    // may have transitioned to CLOSING/CLOSED (e.g. caller closed the raw\n    // socket). Refuse rather than throwing from inside ws.send().\n    if (this._ws.readyState !== WebSocket.OPEN) {\n      throw new Error('BiDi connection is not open')\n    }\n\n    const id = ++this.id\n\n    this._ws.send(JSON.stringify({ id, ...params }))\n\n    return new Promise((resolve, reject) => {\n      const timeoutId = setTimeout(() => {\n        this._pending.delete(id)\n        reject(new Error(`Request with id ${id} timed out`))\n      }, RESPONSE_TIMEOUT)\n\n      this._pending.set(id, { resolve, reject, timeoutId })\n    })\n  }\n\n  /**\n   * Subscribe to events","sourceCodeStart":196,"sourceCodeEnd":232,"githubUrl":"https://github.com/SeleniumHQ/selenium/blob/aa36b38e696a0909e973bdf5e2f9031ffe842c4b/javascript/selenium-webdriver/bidi/index.js#L196-L232","documentation":"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.","triggerScenarios":"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().","commonSituations":"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.","solutions":["Avoid closing the raw socket (`bidi.socket`) directly — use `bidi.close()` which coordinates teardown","Serialize teardown and BiDi commands so close() cannot race an in-flight send()","Retry the command after re-acquiring the connection if the session is still alive"],"exampleFix":"// before — concurrent raw close races the send\nbidi.socket.close()       // raw close\nawait bidi.send(cmd)      // may throw: connection is not open\n\n// after — coordinated close\nawait bidi.close()        // fails pending sends cleanly","handlingStrategy":"try-catch","validationCode":"if (bidi.socket && bidi.socket.readyState === 1 /* OPEN */) {\n  await bidi.send(cmd)\n}","typeGuard":"const isSocketOpen = (bidi) => bidi.socket?.readyState === 1","tryCatchPattern":"try {\n  await bidi.send(cmd)\n} catch (e) {\n  if (/connection is not open/i.test(e.message)) {\n    // transient race — reconnect via driver.getBidi() then retry once\n    return\n  }\n  throw e\n}","preventionTips":["Never call bidi.socket.close() directly — use bidi.close() to coordinate teardown","Serialize connection teardown against in-flight commands","Treat 'not open' as a transient race (reconnect + retry once), unlike the terminal 'is closed'"],"tags":["bidi","websocket","connection","race-condition","runtime"],"backgroundTag":null,"analyzedSha":"aa36b38e696a0909e973bdf5e2f9031ffe842c4b","analyzedAt":"2026-08-14T02:32:32.244Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}