stablyai/orca · error · EmulatorError

emulator_error

emulator_error

Error message

serve-sim AX returned invalid JSON.

What it means

The /ax endpoint returned 2xx but the body was not valid JSON, so JSON.parse threw. The accessibility tree contract is JSON; a non-JSON 2xx body means the response is corrupted, partial, or came from the wrong handler.

Source

Thrown at src/main/emulator/serve-sim-accessibility-tree.ts:27

  try {
    const response = await net.fetch(axUrl, {
      signal: AbortSignal.timeout(AX_REQUEST_TIMEOUT_MS)
    })
    const body = await response.text()
    if (!response.ok) {
      const detail = body.slice(0, MAX_ERROR_BODY_LENGTH) || response.statusText
      const retry = response.status === 503 ? ' Accessibility may still be warming up; retry.' : ''
      throw new EmulatorError(
        'emulator_helper_failed',
        `serve-sim AX request failed (${response.status}): ${detail}.${retry}`
      )
    }

    let tree: unknown
    try {
      tree = JSON.parse(body)
    } catch {
      throw new EmulatorError('emulator_error', 'serve-sim AX returned invalid JSON.')
    }
    if (
      !Array.isArray(tree) ||
      tree.some((node) => typeof node !== 'object' || node === null || Array.isArray(node))
    ) {
      throw new EmulatorError('emulator_error', 'serve-sim AX returned an invalid tree.')
    }
    // serve-sim reports frames in absolute pixels; normalize to 0..1 so the
    // output feeds straight back into tap/gesture.
    return normalizeServeSimAxTree(tree)
  } catch (error) {
    if (error instanceof EmulatorError) {
      throw error
    }
    const detail =
      error instanceof Error && error.name === 'TimeoutError'
        ? 'request timed out'
        : error instanceof Error

View on GitHub (pinned to 1136503c6a)

Solutions

  1. Retry once — transient truncation can clear on the next request.
  2. Update serve-sim to a build matching the current Orca version.
  3. curl the /ax URL directly to inspect what serve-sim actually returned.

Example fix

// before
const tree = await requestServeSimAccessibilityTree(axUrl) // invalid JSON

// after
let tree
try { tree = await requestServeSimAccessibilityTree(axUrl) }
catch (e) { if (isEmulatorError(e, 'emulator_error')) await delay(500); tree = await requestServeSimAccessibilityTree(axUrl) }
Defensive patterns

Strategy: try-catch

Validate before calling

const body = await (await fetch(axUrl)).text()
try { JSON.parse(body) } catch { /* serve-sim returned non-JSON; update/retry */ }

Type guard

function isEmulatorError(e: unknown, code = 'emulator_error'): e is import('./emulator-errors').EmulatorError {
  return e instanceof Error && (e as any).code === code && e.name === 'EmulatorError'
}

Try / catch

try { return await requestServeSimAccessibilityTree(axUrl) }
catch (e) {
  if (isEmulatorError(e, 'emulator_error') && /invalid JSON/.test(e.message)) { await delay(500); return await requestServeSimAccessibilityTree(axUrl) }
  throw e
}

Prevention

When it happens

Trigger: serve-sim-accessibility-tree.ts:24-27 — response.ok is true but `JSON.parse(body)` throws.

Common situations: serve-sim bug returning an HTML/text error page with a 200 status; the response body was truncated by a dropped connection mid-stream; a serve-sim version mismatch returning a different content type.

Understand the failure class

Related errors


AI-assisted analysis of stablyai/orca@1136503c6a (2026-08-12). Data as JSON: /api/errors/22e29deab9c97e2a. Report an issue: GitHub.