jackwener/OpenCLI · error · CommandExecutionError

Twitter device-follow response was missing the expected time

Error message

Twitter device-follow response was missing the expected timeline/globalObjects shape.

What it means

After a successful fetch, the command runs parseDeviceFollow to extract tweets from the timeline/globalObjects structure. If that shape is absent (parser returns falsy), it throws CommandExecutionError because x.com returned JSON that does not match the expected device-follow notification payload.

Source

Thrown at clis/twitter/device-follow.js:173

        } catch (e) {
          return { errorKind: 'exception', detail: String(e && e.message || e) };
        }
      }`);
        if (data?.errorKind === 'non_json') {
            throw new CommandExecutionError(`Twitter device-follow returned non-JSON response: ${data.detail || 'unknown parse error'}`);
        }
        if (data?.errorKind === 'exception') {
            throw new CommandExecutionError(`Twitter device-follow fetch failed: ${data.detail || 'unknown error'}`);
        }
        if (data?.error) {
            if (data.error === 401 || data.error === 403) {
                throw new AuthRequiredError('x.com', `Twitter device-follow returned HTTP ${data.error}`);
            }
            throw new CommandExecutionError(describeTwitterApiError('device_follow', data.error));
        }
        const parsed = parseDeviceFollow(data, new Set());
        if (!parsed) {
            throw new CommandExecutionError('Twitter device-follow response was missing the expected timeline/globalObjects shape.');
        }
        if (parsed.malformedEntries > 0 || parsed.unmatchedTweetEntries > 0) {
            throw new CommandExecutionError('Twitter device-follow entries could not be joined to tweet/user objects.');
        }
        if (parsed.rows.length === 0) {
            throw new EmptyResultError('twitter device-follow', 'No device-follow notification tweets found.');
        }
        const rows = parsed.rows;
        const trimmed = rows.slice(0, limit);
        return applyTopByEngagement(trimmed, kwargs['top-by-engagement']);
    },
});

export const __test__ = {
    buildDeviceFollowUrl,
    extractEntries,
    joinEntryToTweet,
    shapeRow,

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Update/reinstall the CLI in case a newer version parses the new schema
  2. Dump the raw response and compare it against the expected timeline/globalObjects shape
  3. Confirm the authenticated account actually has device-follow notifications enabled
  4. Check for reports of x.com API schema changes if this starts failing consistently
Defensive patterns

Strategy: try-catch

Validate before calling

// No caller-side pre-check; ensure auth first so the payload is the real one
const cookies = await page.getCookies({ url: 'https://x.com' });
if (!cookies.some((c) => c.name === 'ct0')) throw new Error('log in first');

Type guard

function hasTimelineShape(data) {
  return !!data && typeof data === 'object' &&
    ('timeline' in data || 'globalObjects' in data);
}

Try / catch

try {
  await cli.twitter.deviceFollow();
} catch (e) {
  if (e instanceof CommandExecutionError && /timeline\/globalObjects shape/.test(e.message)) {
    console.error('x.com response schema changed — update the CLI.');
    process.exitCode = 1;
  } else throw e;
}

Prevention

When it happens

Trigger: The GraphQL endpoint responds 200 with JSON that lacks the timeline/globalObjects envelope — e.g. an empty body, a different instruction format after a Twitter frontend change, or a soft-error JSON object without the usual error field.

Common situations: Twitter silently changing its GraphQL response schema; a logged-in-but-restricted session returning an alternate payload; hitting a variant endpoint that returns a new response format the parser doesn't know.

Related errors


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