jackwener/OpenCLI · error · CommandExecutionError

expected threads array, got ${typeof threads} (contract drif

Error message

expected threads array, got ${typeof threads} (contract drift?)

What it means

thread-list normalizes the API response with `Array.isArray(data) ? data : (data.threads || [])`; this throw fires when the result is still not an array — meaning `data` was a non-array object without `threads` or a primitive. The CLI treats it as a response contract drift and refuses to map rows.

Source

Thrown at clis/slock/thread-list.js:33

  browser: true,
  siteSession: 'persistent',
  args: [
    { name: 'server', help: 'Override active server' },
  ],
  columns: ['threadChannelId', 'parentMessageId', 'parentChannelName', 'unreadCount', 'replyCount', 'lastReplyAt'],
  func: async (page, kwargs) => {
    await page.goto(SLOCK_HOME_URL);
    const snippet = buildFetchSnippet({
      method: 'GET',
      path: '/channels/threads/followed',
      serverScoped: true,
      serverIdOverride: kwargs.server,
    });
    const result = await page.evaluate(`(async () => { ${snippet} })()`);
    const data = dispatchEvaluateResult(result);
    const threads = Array.isArray(data) ? data : (data.threads || []);
    if (!Array.isArray(threads)) {
      throw new CommandExecutionError(`expected threads array, got ${typeof threads} (contract drift?)`);
    }
    return threads.map((t) => ({
      threadChannelId: t.threadChannelId ?? t.id ?? '',
      parentMessageId: t.parentMessageId ?? '',
      parentChannelName: t.parentChannelName ?? '',
      unreadCount: typeof t.unreadCount === 'number' ? t.unreadCount : 0,
      replyCount: typeof t.replyCount === 'number' ? t.replyCount : null,
      lastReplyAt: t.lastReplyAt ?? null,
    }));
  },
});

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Log `data` (and typeof) to identify the actual envelope shape.
  2. Update dispatchEvaluateResult/snippet to unwrap the new envelope (e.g. `data.threads ?? data.items ?? data.data`).
  3. Pin matching client/server versions.
  4. Check the API base URL and auth — a redirect or error body can masquerade as a success payload.

Example fix

// before
const threads = Array.isArray(data) ? data : (data.threads || []);
// after
const threads = Array.isArray(data) ? data : (data.threads ?? data.items ?? data.data ?? []);
Defensive patterns

Strategy: type-guard

Type guard

function isThreadsEnvelope(d) { return Array.isArray(d) || Array.isArray(d?.threads); }

Try / catch

try {
  const threads = await cli.run(['thread-list']);
} catch (e) {
  if (String(e.message).includes('expected threads array')) {
    console.error('unexpected /threads envelope — check API version and raw payload');
  } else throw e;
}

Prevention

When it happens

Trigger: GET threads returns an envelope the CLI doesn't know ({data:[...]}, {items:[...]}), an error object that passed dispatchEvaluateResult, or a string like "ok" instead of a JSON array.

Common situations: API version change renaming the `threads` key; a gateway error page served with 200; hitting the wrong endpoint path after a base-URL change.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/4bbdf7a52565affb. Report an issue: GitHub.