jackwener/OpenCLI · warning · EmptyResultError

twitter download

Error message

twitter download ${target.id}

What it means

The `twitter download <tweet-id>` command threw EmptyResultError because its in-page media extraction script returned an array with zero media items. The library throws this when the tweet parsed successfully but contains no downloadable media (images/videos), or when extraction silently found nothing on the live page. It deliberately distinguishes 'no data' from malformed payloads (which raise CommandExecutionError).

Solutions

  1. Verify the tweet actually has images or video by opening https://x.com/<user>/status/<id> in a browser.
  2. Log into x.com in the connected browser session so protected/age-gated media is visible, then retry.
  3. Retry with a fresh page load (not a cached or limited view) in case media was not rendered.
  4. If media is visibly present but the error persists, the extraction selectors are likely stale — update the CLI or report the DOM change.

Example fix

// before
opencli twitter download 1234567890 // text-only tweet
// after
opencli twitter download 9876543210 // tweet with attached images/video
Defensive patterns

Strategy: validation

Validate before calling

// Extracted media array guard before consuming results
function hasMedia(items) {
  return Array.isArray(items) && items.length > 0 && items.every((m) => typeof m?.url === 'string');
}
// Run twitter download only for tweets you know embed media; pre-check the tweet
// (e.g. oEmbed or the profile page) to confirm media presence before invoking.

Type guard

function isMediaItem(m) {
  return typeof m === 'object' && m !== null && typeof m.url === 'string' && m.url.length > 0;
}

Try / catch

try {
  const rows = await twitterDownload(tweetId);
} catch (e) {
  if (e.code === 'EMPTY_RESULT') {
    // Tweet has no media — skip, don't retry
    logger.warn(`No media in tweet ${tweetId}, skipping`);
  } else throw e;
}

Prevention

When it happens

Trigger: Running `opencli twitter download <id>` on a tweet with no media attachments; extraction against a protected/deleted tweet where the DOM renders but yields no media nodes; X UI changes making the extractor's selectors miss media so items.length === 0.

Common situations: Passing a tweet ID for a text-only tweet; scraping a protected or age-gated account while logged out; X shipping a redesign that breaks extraction selectors; a rate-limited view of the tweet that omits media.

Understand the failure class

Background: EmptyResultError / "no results found": when an API or scraper succeeds but returns zero rows — this error's family across 9 libraries.

Related errors


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

Appendix: source

Thrown at clis/twitter/download.js:453

          out.push({ type: 'image', url: src });
        });
        document.querySelectorAll('video').forEach(video => {
          const src = video.src || '';
          if (src) out.push({ type: 'video', url: src });
        });
        document.querySelectorAll('[data-testid="videoPlayer"]').forEach(player => {
          const tweetLink = player.closest('article')?.querySelector('a[href*="/status/"]');
          const href = tweetLink?.getAttribute('href') || '';
          if (href) out.push({ type: 'video-tweet', url: 'https://x.com' + href });
        });
        return out;
      })()
    `));
    if (!Array.isArray(items)) {
        throw new CommandExecutionError('Twitter tweet media extraction returned malformed payload');
    }
    if (items.length === 0) {
        throw new EmptyResultError(`twitter download ${target.id}`, 'No media found in the tweet');
    }
    const cookies = await page.getCookies({ domain: 'x.com' });
    const seen = new Set();
    const unique = items.filter((m) => {
        if (seen.has(m.url)) return false;
        seen.add(m.url);
        return true;
    }).map((m) => {
        return { ...m, tweet_id: target.id };
    });
    return downloadTwitterMedia(unique, {
        output,
        subdir: 'tweets',
        cookies: formatCookieHeader(cookies),
        browserCookies: cookies,
        filenamePrefix: 'tweet',
        ytdlpExtraArgs: ['--merge-output-format', 'mp4'],
    });

View on GitHub (pinned to 49907e53dc)