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

  1. Update the repository/provider to the latest version — the parser is usually fixed promptly after t.me markup changes.
  2. Open https://t.me/s/<channel> in a browser and inspect the post containers; if the class names changed, update the selectors in parseChannelPage().
  3. Retry later or from a different IP — Telegram sometimes serves interstitial/challenge pages that break parsing.
  4. 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

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


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)