moeru-ai/airi · error · Error

AIRI host websocket bridge is unavailable

Error message

AIRI host websocket bridge is unavailable

What it means

The low-level throw inside postBridgeMessage in stage-pocket's websocket-bridge module. postBridgeMessage is called for connect/send/close commands and throws when neither window.AiriHostBridge nor window.webkit.messageHandlers.airiHostBridge is present. This is the root cause that the probe-level errors [0]/[1] guard against; reaching it directly means a bridge command was issued without the native shell being available.

Source

Thrown at apps/stage-pocket/src/modules/websocket-bridge.ts:45

      onNativeMessage?: (payload: string) => void
    }
  }
}

const connections = new Map<string, HostBridgeConnection>()

function postBridgeMessage(command: HostBridgeCommand) {
  if (window.AiriHostBridge) {
    window.AiriHostBridge.postMessage(JSON.stringify(command))
    return
  }

  if (window.webkit?.messageHandlers?.airiHostBridge) {
    window.webkit.messageHandlers.airiHostBridge.postMessage(JSON.stringify(command))
    return
  }

  throw new Error('AIRI host websocket bridge is unavailable')
}

function dispatchNativeEvent(payload: string) {
  const event = JSON.parse(payload) as HostBridgeEvent
  const connection = connections.get(event.id)
  if (!connection) {
    return
  }

  connection.handleNativeEvent(event)
}

class HostBridgeConnection {
  readonly id = crypto.randomUUID()
  private opened = false
  private settled = false

  constructor(

View on GitHub (pinned to 27111382b4)

Solutions

  1. Always obtain the connector through getHostWebSocketConnector and discard it if the native shell reloads.
  2. In tests, stub window.AiriHostBridge = { postMessage: () => {} } before exercising connection logic.
  3. Guard send/close paths so they no-op when the bridge has disappeared rather than throwing.

Example fix

// before
postBridgeMessage({ kind: 'send', id, data })

// after
if (!window.AiriHostBridge && !window.webkit?.messageHandlers?.airiHostBridge) {
  return false
}
postBridgeMessage({ kind: 'send', id, data })
Defensive patterns

Strategy: validation

Validate before calling

function assertBridgeForCommand(): boolean {
  return !!(window.AiriHostBridge || window.webkit?.messageHandlers?.airiHostBridge)
}

// guard send/close paths:
if (!assertBridgeForCommand()) return false

Type guard

function isHostBridgeInjected(): boolean {
  return !!(window.AiriHostBridge || window.webkit?.messageHandlers?.airiHostBridge)
}

Prevention

When it happens

Trigger: Any HostBridgeConnection operation (constructor connect, send, or close) invoked when the native bridge globals are absent. getHostWebSocketConnector already short-circuits to undefined in that case, so hitting this raw throw implies a caller bypassed that check or the bridge vanished after the connector was created.

Common situations: Connector obtained while the bridge existed, then a send/close issued after the webview was reloaded or the native shell detached the handler; unit tests that construct the connector without mocking the globals; a code path that calls postBridgeMessage directly.

Related errors


AI-assisted analysis of moeru-ai/airi@27111382b4 (2026-08-12). Data as JSON: /api/errors/2b4b5654fe1d36cd. Report an issue: GitHub.