santifer/career-ops · error · Error

telegram-channel: @ has no public preview (private channel…

Error message

telegram-channel: @${channel} has no public preview (private channel, or preview switched off) — it cannot be read without an authenticated Telegram integration

What it means

The telegram-channel provider fetches a channel's public web preview at t.me/s/<channel>. Telegram only serves that preview for public channels with previews enabled; when the first page's HTML contains no preview content, the provider throws because it cannot read the channel at all without an authenticated Telegram client (Bot/MTProto API). This is an unconditional capability failure, not a transient network problem.

Solutions

  1. Open https://t.me/s/<channel> in a browser; if no posts render, the channel genuinely has no public preview and this provider cannot read it.
  2. Verify the @handle is spelled correctly and the channel exists (search it in the Telegram app).
  3. If the channel is intentionally private, switch to an authenticated Telegram integration (Bot API or MTProto client such as GramJS) instead of the public-preview scraper.
  4. Remove or disable the entry in portals.yml if the source is not readable publicly.

Example fix

// before
{ name: 'my-private-channel', provider: 'telegram-channel', channel: '@internals' }
// after
// channel must be public with preview enabled, e.g.
{ name: 'my-channel', provider: 'telegram-channel', channel: '@publicjobs' }
Defensive patterns

Strategy: validation

Validate before calling

// before configuring a telegram-channel entry, verify a public preview exists
async function hasPublicPreview(channel) {
  const res = await fetch(`https://t.me/s/${channel.replace(/^@/, '')}`);
  const html = await res.text();
  return res.ok && html.includes('tgme_widget_message');
}
// only add the entry if hasPublicPreview('@channel') === true

Type guard

function isPublicTelegramChannel(entry) {
  return typeof entry.channel === 'string' && /^@[A-Za-z0-9_]{4,}$/.test(entry.channel);
}
// plus a runtime preview check as above before enabling the entry

Try / catch

try {
  await telegramProvider.fetch(entry, ctx);
} catch (err) {
  if (String(err.message).includes('no public preview')) {
    console.warn(`Skipping ${entry.name}: not publicly readable — needs an authenticated Telegram integration`);
    return null; // skip, don't fail the scan
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling fetch() for a @channel where the t.me/s page (page 0) parses zero posts and parseChannelPage() reports noPreview=true — i.e. the channel is private, the public preview has been disabled in channel settings, or the handle does not exist.

Common situations: Configuring portals.yml with a private or deleted Telegram channel; a channel admin turning off 'public preview' after the entry was added; a typo in the @handle pointing at a non-existent channel.

Understand the failure class

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/9330d07037dc7693. Report an issue: GitHub.

Appendix: source

Thrown at providers/telegram-channel.mjs:398

    let reachedCutoff = false;
    let page = 0;
    for (; page < maxPages; page++) {
      if (page > 0) await sleep(PAGE_DELAY_MS, ctx);
      const url = `https://t.me/s/${channel}${before === null ? '' : `?before=${before}`}`;
      let html;
      try {
        // redirect:'manual' is never followed, but the 3xx keeps its status and
        // Location, so a private channel is a named failure, not "fetch failed".
        html = await fetchTextWithRetry(ctx, url, { redirect: 'manual' });
      } catch (err) {
        if (page === 0 && err?.status >= 300 && err.status < 400) {
          throw new Error(`telegram-channel: @${channel} has no public preview — private channel, preview switched off, or no such channel (t.me answered ${err.status}${err.location ? ` → ${err.location}` : ''}). It cannot be read without an authenticated Telegram integration.`);
        }
        throw err;
      }
      const { posts, noPreview, textPosts } = parseChannelPage(html, channel);
      if (page === 0 && noPreview) {
        throw new Error(`telegram-channel: @${channel} has no public preview (private channel, or preview switched off) — it cannot be read without an authenticated Telegram integration`);
      }
      if (page === 0 && posts.length === 0 && textPosts > 0) {
        throw new Error(`telegram-channel: @${channel} served ${textPosts} text posts but none parsed — t.me markup changed`);
      }
      if (posts.length === 0) break;
      const oldest = Math.min(...posts.map((p) => p.id));
      if (before !== null && oldest >= before) break; // t.me re-served the same page
      read += posts.length;
      for (const post of posts) {
        if (post.postedAt !== undefined && post.postedAt < cutoff) { reachedCutoff = true; continue; }
        const job = postToJob(post);
        if (job) jobs.push(job);
      }
      if (reachedCutoff) break;
      before = oldest;
    }
    // Cap warning (same pattern as a16z-speedrun-talent/jibeapply/workday):
    // the window was not reached, so the inventory is a prefix. Silent under

View on GitHub (pinned to aac998c7ed)