{"record":{"id":"0fd1810c53cb7237","repo":"santifer/career-ops","slug":"telegram-channel-channel-served-textposts-text-posts-but","errorCode":null,"errorMessage":"telegram-channel: @${channel} served ${textPosts} text posts but none parsed — t.me markup changed","messagePattern":"telegram-channel: @(.+?) served (.+?) text posts but none parsed — t\\.me markup changed","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/telegram-channel.mjs","lineNumber":401,"sourceCode":"      if (page > 0) await sleep(PAGE_DELAY_MS, ctx);\n      const url = `https://t.me/s/${channel}${before === null ? '' : `?before=${before}`}`;\n      let html;\n      try {\n        // redirect:'manual' is never followed, but the 3xx keeps its status and\n        // Location, so a private channel is a named failure, not \"fetch failed\".\n        html = await fetchTextWithRetry(ctx, url, { redirect: 'manual' });\n      } catch (err) {\n        if (page === 0 && err?.status >= 300 && err.status < 400) {\n          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.`);\n        }\n        throw err;\n      }\n      const { posts, noPreview, textPosts } = parseChannelPage(html, channel);\n      if (page === 0 && noPreview) {\n        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`);\n      }\n      if (page === 0 && posts.length === 0 && textPosts > 0) {\n        throw new Error(`telegram-channel: @${channel} served ${textPosts} text posts but none parsed — t.me markup changed`);\n      }\n      if (posts.length === 0) break;\n      const oldest = Math.min(...posts.map((p) => p.id));\n      if (before !== null && oldest >= before) break; // t.me re-served the same page\n      read += posts.length;\n      for (const post of posts) {\n        if (post.postedAt !== undefined && post.postedAt < cutoff) { reachedCutoff = true; continue; }\n        const job = postToJob(post);\n        if (job) jobs.push(job);\n      }\n      if (reachedCutoff) break;\n      before = oldest;\n    }\n    // Cap warning (same pattern as a16z-speedrun-talent/jibeapply/workday):\n    // the window was not reached, so the inventory is a prefix. Silent under\n    // ctx.maxPages, which is verify-portals asking for one page on purpose.\n    if (page >= maxPages && !reachedCutoff && maxPages === entryPages) {\n      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`);","sourceCodeStart":383,"sourceCodeEnd":419,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/telegram-channel.mjs#L383-L419","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before (stale parser)\nconst nodes = doc.querySelectorAll('.tgme_widget_message_wrap .tgme_widget_message_text');\n// after (markup changed — adjust to new classes)\nconst nodes = doc.querySelectorAll('.tgme_widget_message_wrap .tgme_widget_message.bubble .tgme_widget_message_text');","handlingStrategy":"fallback","validationCode":"// detect the condition before it aborts a larger scan\nconst looksLikeChannelPage = html.includes('tgme_widget_message');\nconst parsedSome = posts.length > 0;\nif (looksLikeChannelPage && !parsedSome) {\n  console.warn('telegram markup drift detected — flag for parser update');\n}","typeGuard":"function parseLooksHealthy(result) {\n  return result && typeof result === 'object'\n    && Array.isArray(result.posts)\n    && (result.posts.length > 0 || result.textPosts === 0);\n}","tryCatchPattern":"try {\n  return await telegramProvider.fetch(entry, ctx);\n} catch (err) {\n  if (String(err.message).includes('markup changed')) {\n    console.error(`Parser out of date for ${entry.name}; check for a provider update`);\n    return { posts: [], degraded: true };\n  }\n  throw err;\n}","preventionTips":["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"],"tags":["telegram","scraping","parser","markup-changed"],"backgroundTag":"unexpected-response-shape","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}