santifer/career-ops · error · Error

telegram-channel: " " needs a channel handle (5-32 letters…

Error message

telegram-channel: "${entry.name || '?'}" needs a channel handle (5-32 letters, digits or underscores, no @), got ${JSON.stringify(entry.channel)}

What it means

The telegram-channel provider fetches t.me/s/<handle> public previews, so the channel handle is the only interpolated value and is pinned by CHANNEL_RE to 5-32 letters/digits/underscores. An entry whose channel field is missing, empty, or malformed (e.g. contains '@', spaces, dashes, or a full URL) fails this validation with the raw JSON of what was provided.

Solutions

  1. Set channel to just the handle: channel: mychannel (5-32 [A-Za-z0-9_], no @, no URL)
  2. If the channel id contains characters outside the pattern, confirm it is a valid public channel handle
  3. Remove @-prefix confusion — it is stripped automatically, the raw handle is expected

Example fix

// before
channel: 'https://t.me/my_channel'
// after
channel: 'my_channel'
Defensive patterns

Strategy: validation

Validate before calling

const CHANNEL_RE = /^[A-Za-z0-9_]{5,32}$/;
const ch = String(entry.channel ?? '').replace(/^@/, '').trim();
if (!CHANNEL_RE.test(ch)) {
  throw new Error(`${entry.name}: channel must be a bare handle (5-32 letters/digits/underscores), got ${JSON.stringify(entry.channel)}`);
}

Type guard

function isValidTelegramChannel(entry) {
  const ch = String(entry?.channel ?? '').replace(/^@/, '').trim();
  return /^[A-Za-z0-9_]{5,32}$/.test(ch);
}

Try / catch

try {
  offers = await provider.fetch(entry, ctx);
} catch (e) {
  if (e.message.startsWith('telegram-channel:') && e.message.includes('needs a channel handle')) {
    console.warn(`${entry.name}: fix the channel field — bare handle only, no URL`);
    return [];
  }
  throw e;
}

Prevention

When it happens

Trigger: portals.yml entry omits channel; channel: '@mychannel' (the @ is stripped before the check, so a bare @ is fine, but e.g. 'channel: https://t.me/mychannel' or 'my channel' fails); channel contains a hyphen or is shorter than 5 chars.

Common situations: Pasting the full t.me URL instead of the handle; usernames with dashes (Telegram allows them for some bots but not per this strict pattern); typo leaving the value empty due to YAML quoting issues.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


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

Appendix: source

Thrown at providers/telegram-channel.mjs:363

  const n = Number(raw);
  if (!Number.isInteger(n) || n < 1) return fallback;
  return Math.min(n, cap);
}

/** @type {Provider} */
export default {
  id: 'telegram-channel',

  /**
   * @param {{ name?: string, channel?: string, max_pages?: number|string, since_days?: number|string }} entry
   * @param {{ fetchText: Function, sleep?: Function, maxPages?: number, sinceMs?: number }} ctx
   */
  async fetch(entry, ctx) {
    const channel = String(entry.channel || '').replace(/^@/, '').trim();
    // The handle is the only value that reaches the URL, and the pattern pins
    // it to t.me/s/<handle>: no path separators, no query, no other host.
    if (!CHANNEL_RE.test(channel)) {
      throw new Error(`telegram-channel: "${entry.name || '?'}" needs a channel handle (5-32 letters, digits or underscores, no @), got ${JSON.stringify(entry.channel)}`);
    }
    // Pages are walked until the window is reached, so the inventory inside
    // `since_days` is complete (Source Indexing Policy, rule 3), up to a cap
    // that keeps one channel under ~5 s. ctx.maxPages (verify-portals) wins.
    const entryPages = pageCount(entry.max_pages, MAX_PAGES_CAP, MAX_PAGES_CAP);
    const maxPages = Math.min(entryPages, ctx.maxPages ?? Infinity);
    const sinceDays = Number(entry.since_days);
    const windowDays = Number.isFinite(sinceDays) && sinceDays > 0 ? sinceDays : DEFAULT_SINCE_DAYS;
    // The scanner's own early-stop window (max_posting_age_days) can only be
    // narrower than the entry's; stop at whichever comes first.
    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;

View on GitHub (pinned to aac998c7ed)