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
- 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.
- Verify the @handle is spelled correctly and the channel exists (search it in the Telegram app).
- 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.
- 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
- Check t.me/s/<channel> renders posts in a browser before adding any channel to portals.yml
- Re-verify channel entries periodically — admins can disable previews at any time
- Keep a list of private channels that require a Bot/MTProto integration and route them elsewhere
- Double-check @handles for typos; a wrong handle usually means no preview
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
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- telegram-channel: @ served text posts but none parsed —…
- a16z-speedrun-talent: invalid URL
- agentic-jobs: invalid URL
- avature: still contains JobDetail links but no article…
- capacity must be a finite number >= 1, got
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 underView on GitHub (pinned to aac998c7ed)