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
- Set channel to just the handle: channel: mychannel (5-32 [A-Za-z0-9_], no @, no URL)
- If the channel id contains characters outside the pattern, confirm it is a valid public channel handle
- 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
- Store the bare handle, never the full t.me URL
- Test new entries by opening https://t.me/s/<handle> in a browser
- Quote YAML values to avoid parsing surprises
- Check for Telegram handles with dashes — they don't match the strict pattern
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
- apify: entry has invalid field_map. Each of title, url…
- apify: entry missing 'actor' (e.g. misceres/indeed-scraper)
- arbeitnow: invalid URL
- ashby: invalid URL
- bamboohr: invalid URL
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)