santifer/career-ops · error · Error

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

Error message

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.

What it means

The provider requests t.me/s/<handle> with redirect:'manual' so a private (or nonexistent) channel's 3xx response is detected instead of silently followed. On the first page, any 3xx-400 response triggers this explicit message: the channel has no public preview and cannot be read without an authenticated Telegram integration.

Solutions

  1. Verify the handle exists and is public by opening https://t.me/s/<handle> in a browser
  2. Enable public preview in the channel settings, or use a different public channel/mirror
  3. If the channel must stay private, integrate an authenticated Telegram client (MTProto/Bot API) instead of this provider
  4. Fix typos — a wrong handle often manifests as this redirect

Example fix

// before
channel: 'old_handle'  // t.me 302 → t.me/renamed_handle
// after
channel: 'renamed_handle'
Defensive patterns

Strategy: fallback

Validate before calling

null

Type guard

null

Try / catch

try {
  offers = await provider.fetch(entry, ctx);
} catch (e) {
  if (e.message.includes('has no public preview')) {
    console.warn(`${entry.name}: @${entry.channel} is private/preview-off — skipping or falling back to another source`);
    return [];
  }
  throw e;
}

Prevention

When it happens

Trigger: Channel is private; public preview (restrict saving / preview) is disabled in channel settings; the handle doesn't exist and t.me responds with a redirect; t.me returns 302 to a login or error location.

Common situations: Company moved to a private internal channel; channel renamed so old handle redirects; channel was always invite-only; typo pointing at a nonexistent handle that t.me redirects to an error page.

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


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

Appendix: source

Thrown at providers/telegram-channel.mjs:392

    const cutoff = Math.max(Date.now() - windowDays * DAY_MS, Number.isFinite(ctx.sinceMs) ? Number(ctx.sinceMs) : 0);
    const label = entry.name || `@${channel}`;

    const jobs = [];
    let before = null;
    let read = 0;
    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);

View on GitHub (pinned to aac998c7ed)