jackwener/OpenCLI · error · CommandExecutionError

twitter_likes_protocol_error

twitter_likes_protocol_error

Error message

twitter_likes_protocol_error: missing Likes timeline instructions

What it means

CommandExecutionError (code twitter_likes_protocol_error) thrown when the Likes API responds OK but the payload contains no timeline instructions and does not look like a private-timeline response. This means Twitter returned an unrecognized payload shape, so parsing cannot proceed; the library treats it as a scraping-protocol break rather than an empty result.

Source

Thrown at clis/twitter/likes.js:318

            const fetchCount = Math.min(100, remaining);
            const apiUrl = buildLikesUrl(likesQueryId, userId, fetchCount, cursor);
            const data = unwrapBrowserResult(await page.evaluate(`async () => {
        const r = await fetch(${JSON.stringify(apiUrl)}, { headers: ${headers}, credentials: 'include' });
        return r.ok ? await r.json() : { error: r.status };
      }`));
            if (data?.error) {
                if ((useOutputFile ? outputCount : allTweets.length) === 0)
                    throw new CommandExecutionError(describeTwitterApiError('Likes', data.error));
                break;
            }
            lastRawResponse = data;
            const hasInstructions = Array.isArray(data?.data?.user?.result?.timeline_v2?.timeline?.instructions)
                || Array.isArray(data?.data?.user?.result?.timeline?.timeline?.instructions);
            if (!hasInstructions) {
                if (looksLikePrivateTwitterTimeline(data) && (useOutputFile ? outputCount : allTweets.length) === 0) {
                    throw new EmptyResultError('twitter likes', `No likes returned for @${username} (Likes are private by default on X; only the account owner can view their own likes)`);
                }
                throw new CommandExecutionError('twitter_likes_protocol_error: missing Likes timeline instructions');
            }
            const { tweets, nextCursor } = parseLikes(data, seen);
            if (useOutputFile) {
                appendJsonlRows(outputFile, tweets);
                outputCount += tweets.length;
            }
            else {
                allTweets.push(...tweets);
            }
            const pageComplete = !nextCursor;
            writeResumeFile(resumeFile, {
                cursor: pageComplete ? null : nextCursor,
                count: useOutputFile ? outputCount : allTweets.length,
                tweets: useOutputFile ? undefined : allTweets,
                updatedAt: new Date().toISOString(),
                complete: pageComplete,
                source: 'likes',
                username,

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Log the raw response (lastRawResponse) and inspect the actual JSON shape.
  2. Re-resolve the Likes queryId with resolveTwitterQueryId to ensure you hit the current GraphQL operation.
  3. Update parseLikes and the instructions paths in likes.js to match Twitter's new schema.
  4. Re-authenticate; a degraded session can yield non-timeline payloads.

Example fix

// before
const instructions = data?.data?.user?.result?.timeline_v2?.timeline?.instructions;
// after
const instructions = data?.data?.user?.result?.timeline_v2?.timeline?.instructions
  || data?.data?.user?.result?.timeline?.timeline?.instructions
  || data?.data?.user?.result?.timeline?.instructions; // accommodate new/legacy shapes
Defensive patterns

Strategy: try-catch

Validate before calling

// after a probe request, verify the response shape before the full run
const probe = await fetchLikesPage(cursor = null);
const ok = Array.isArray(probe?.data?.user?.result?.timeline_v2?.timeline?.instructions)
  || Array.isArray(probe?.data?.user?.result?.timeline?.timeline?.instructions);
if (!ok) console.warn('Unexpected Likes payload shape; update parser/queryId.');

Type guard

function hasTimelineInstructions(data) {
  const r = data?.data?.user?.result;
  return Array.isArray(r?.timeline_v2?.timeline?.instructions)
    || Array.isArray(r?.timeline?.timeline?.instructions);
}

Try / catch

try {
  await cli.twitter.likes({ username });
} catch (e) {
  if (e.code === 'twitter_likes_protocol_error') {
    console.error('Twitter response schema changed; dump lastRawResponse and update parseLikes/query IDs.');
  } else throw e;
}

Prevention

When it happens

Trigger: After the API call at clis/twitter/likes.js:318, Array.isArray checks on data?.data?.user?.result?.timeline_v2?.timeline?.instructions and the legacy timeline path both fail, and looksLikePrivateTwitterTimeline is false (or some rows were already collected).

Common situations: Twitter changed the Likes GraphQL response schema; a stale queryId serving a different response shape; the account returned a guest/limited payload; soft-blocked requests returning ok-with-empty-body responses.

Related errors


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