{"record":{"id":"2b4b5654fe1d36cd","repo":"moeru-ai/airi","slug":"airi-host-websocket-bridge-is-unavailable-2b4b56","errorCode":null,"errorMessage":"AIRI host websocket bridge is unavailable","messagePattern":"AIRI host websocket bridge is unavailable","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/stage-pocket/src/modules/websocket-bridge.ts","lineNumber":45,"sourceCode":"      onNativeMessage?: (payload: string) => void\n    }\n  }\n}\n\nconst connections = new Map<string, HostBridgeConnection>()\n\nfunction postBridgeMessage(command: HostBridgeCommand) {\n  if (window.AiriHostBridge) {\n    window.AiriHostBridge.postMessage(JSON.stringify(command))\n    return\n  }\n\n  if (window.webkit?.messageHandlers?.airiHostBridge) {\n    window.webkit.messageHandlers.airiHostBridge.postMessage(JSON.stringify(command))\n    return\n  }\n\n  throw new Error('AIRI host websocket bridge is unavailable')\n}\n\nfunction dispatchNativeEvent(payload: string) {\n  const event = JSON.parse(payload) as HostBridgeEvent\n  const connection = connections.get(event.id)\n  if (!connection) {\n    return\n  }\n\n  connection.handleNativeEvent(event)\n}\n\nclass HostBridgeConnection {\n  readonly id = crypto.randomUUID()\n  private opened = false\n  private settled = false\n\n  constructor(","sourceCodeStart":27,"sourceCodeEnd":63,"githubUrl":"https://github.com/moeru-ai/airi/blob/27111382b4a79a7e983289d6e983a06af185ed0f/apps/stage-pocket/src/modules/websocket-bridge.ts#L27-L63","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Always obtain the connector through getHostWebSocketConnector and discard it if the native shell reloads.","In tests, stub window.AiriHostBridge = { postMessage: () => {} } before exercising connection logic.","Guard send/close paths so they no-op when the bridge has disappeared rather than throwing."],"exampleFix":"// before\npostBridgeMessage({ kind: 'send', id, data })\n\n// after\nif (!window.AiriHostBridge && !window.webkit?.messageHandlers?.airiHostBridge) {\n  return false\n}\npostBridgeMessage({ kind: 'send', id, data })","handlingStrategy":"validation","validationCode":"function assertBridgeForCommand(): boolean {\n  return !!(window.AiriHostBridge || window.webkit?.messageHandlers?.airiHostBridge)\n}\n\n// guard send/close paths:\nif (!assertBridgeForCommand()) return false","typeGuard":"function isHostBridgeInjected(): boolean {\n  return !!(window.AiriHostBridge || window.webkit?.messageHandlers?.airiHostBridge)\n}","tryCatchPattern":null,"preventionTips":["Always obtain connectors through getHostWebSocketConnector; never call postBridgeMessage directly.","No-op send/close when the bridge disappears instead of throwing.","Stub the bridge global in unit tests."],"tags":["websocket","native-bridge","stage-pocket","runtime-environment"],"backgroundTag":null,"analyzedSha":"27111382b4a79a7e983289d6e983a06af185ed0f","analyzedAt":"2026-08-12T18:33:34.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}