santifer/career-ops · error · Error
telegram-channel: @ served text posts but none parsed —…
Error message
telegram-channel: @${channel} served ${textPosts} text posts but none parsed — t.me markup changed What it means
After fetching the t.me/s channel page, the provider counts elements that look like text posts but yields zero parsed post objects. That means the HTML downloaded is non-empty, yet the parser's selectors no longer match — the canonical signal that Telegram changed the t.me markup and this scraper version is out of date. It throws only on page 0 so partial later-page failures don't abort a working scan.
Solutions
- Update the repository/provider to the latest version — the parser is usually fixed promptly after t.me markup changes.
- Open https://t.me/s/<channel> in a browser and inspect the post containers; if the class names changed, update the selectors in parseChannelPage().
- Retry later or from a different IP — Telegram sometimes serves interstitial/challenge pages that break parsing.
- If urgent, add a temporary parser patch matching the new markup and report it upstream.
Example fix
// before (stale parser)
const nodes = doc.querySelectorAll('.tgme_widget_message_wrap .tgme_widget_message_text');
// after (markup changed — adjust to new classes)
const nodes = doc.querySelectorAll('.tgme_widget_message_wrap .tgme_widget_message.bubble .tgme_widget_message_text'); Defensive patterns
Strategy: fallback
Validate before calling
// detect the condition before it aborts a larger scan
const looksLikeChannelPage = html.includes('tgme_widget_message');
const parsedSome = posts.length > 0;
if (looksLikeChannelPage && !parsedSome) {
console.warn('telegram markup drift detected — flag for parser update');
} Type guard
function parseLooksHealthy(result) {
return result && typeof result === 'object'
&& Array.isArray(result.posts)
&& (result.posts.length > 0 || result.textPosts === 0);
} Try / catch
try {
return await telegramProvider.fetch(entry, ctx);
} catch (err) {
if (String(err.message).includes('markup changed')) {
console.error(`Parser out of date for ${entry.name}; check for a provider update`);
return { posts: [], degraded: true };
}
throw err;
} Prevention
- Keep the repo/provider updated — t.me markup changes are usually patched quickly upstream
- Alert on this specific error message so markup drift is noticed within one scan cycle
- Test the parser against a saved fixture page after each Telegram web redesign
- Pin and monitor a canary channel whose posts you know exist, so drift is detected early
When it happens
Trigger: fetch() on page 0 returns HTML where textPosts > 0 but parseChannelPage() produces posts.length === 0 — Telegram's DOM structure for t.me/s channels changed so the parser's selectors extract nothing.
Common situations: Telegram ships a t.me redesign; an outdated checkout of the repo whose parser predates the new markup; an A/B-tested layout variant served to some requests; a proxy/CDN (or Telegram's abuse interstitial) replacing normal post markup while still returning text nodes.
Related errors
- rheinmetall: page 1 returned no vacancy cards for
- telegram-channel: @ has no public preview (private channel…
- avature: still contains JobDetail links but no article…
- csod: no anonymous token on
- [fetch] Playwright error
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/0fd1810c53cb7237.
Report an issue: GitHub.
Appendix: source
Thrown at providers/telegram-channel.mjs:401
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
// ctx.maxPages, which is verify-portals asking for one page on purpose.
if (page >= maxPages && !reachedCutoff && maxPages === entryPages) {
console.error(`⚠️ telegram-channel: ${label} truncated at max_pages=${maxPages} (${read} posts read, none older than since_days=${windowDays} yet) — raise max_pages on this entry for the full window`);View on GitHub (pinned to aac998c7ed)