jackwener/OpenCLI · error · CommandExecutionError

Twitter UserByScreenName returned GraphQL errors: ${JSON.str

Error message

Twitter UserByScreenName returned GraphQL errors: ${JSON.stringify(normalizedUserLookup.errors).slice(0, 200)}

What it means

The UserByScreenName GraphQL lookup can succeed at HTTP level but return a payload containing an `errors` array (Twitter's GraphQL error channel). The code normalizes the payload and, if errors exist, throws a CommandExecutionError including up to 200 characters of the serialized errors for diagnosis.

Source

Thrown at clis/twitter/download.js:376

        'X-Csrf-Token': ct0,
        'X-Twitter-Auth-Type': 'OAuth2Session',
        'X-Twitter-Active-User': 'yes',
    });

    const ubsUrl = buildUserByScreenNameUrl(userByScreenNameOperation, username);
    const userLookup = requireFetchPayload(await page.evaluate(`async () => {
      try {
        const resp = await fetch("${ubsUrl}", { headers: ${headers}, credentials: 'include' });
        if (!resp.ok) return { ok: false, status: resp.status };
        const payload = await resp.json();
        return { ok: true, payload };
      } catch (err) {
        return { ok: false, error: err?.message ?? String(err) };
      }
    }`));
    const normalizedUserLookup = normalizeTwitterGraphqlPayload(userLookup);
    if (Array.isArray(normalizedUserLookup?.errors) && normalizedUserLookup.errors.length > 0) {
        throw new CommandExecutionError(`Twitter UserByScreenName returned GraphQL errors: ${JSON.stringify(normalizedUserLookup.errors).slice(0, 200)}`);
    }
    const userId = normalizedUserLookup?.data?.user?.result?.rest_id;
    if (!userId) throw new EmptyResultError(`twitter download @${username}`, `Could not resolve @${username}`);

    const seen = new Set();
    const all = [];
    let cursor = null;
    let hasMorePages = false;
    for (let i = 0; i < MAX_PAGINATION_PAGES && all.length < limit; i++) {
        const fetchCount = nextUserMediaFetchCount(limit, all.length);
        if (fetchCount === 0) break;
        const url = buildUserMediaUrl(userMediaOperation, userId, fetchCount, cursor);
        const data = normalizeTwitterGraphqlPayload(requireFetchPayload(await page.evaluate(`async () => {
        try {
          const r = await fetch("${url}", { headers: ${headers}, credentials: 'include' });
          if (!r.ok) return { ok: false, status: r.status };
          return { ok: true, payload: await r.json() };
        } catch (err) {

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Verify the username exists and is not suspended by visiting https://x.com/<username>
  2. Re-run after a delay to rule out transient rate limiting
  3. Refresh/update the CLI so resolveTwitterOperationMetadata picks up current UserByScreenName operation metadata
  4. Re-login to the browser session to ensure the CSRF token is valid
Defensive patterns

Strategy: try-catch

Validate before calling

const res = await fetch(url, { headers, credentials: 'include' });
if (!res.ok) throw new Error(`UserByScreenName HTTP ${res.status}`);

Type guard

const hasGraphqlErrors = (payload) => Array.isArray(payload?.errors) && payload.errors.length > 0;

Try / catch

try {
  await cmd();
} catch (err) {
  if (err.name === 'CommandExecutionError' && err.message.includes('GraphQL errors')) {
    // inspect err.message for error code (e.g. 144 user not found) and back off/retry
  }
}

Prevention

When it happens

Trigger: Fetching the UserByScreenName endpoint when the account is suspended or doesn't exist, the queryId/operation metadata is stale (x.com deploy changed it), the CSRF token is invalid, or Twitter rate-limits/forbids the request.

Common situations: Typo in the username (handle not found); account suspended/deactivated; x.com shipped a new GraphQL operation hash making the cached metadata rejected; too many rapid requests triggering server-side errors.

Related errors


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