jackwener/OpenCLI · warning · CliError
NO_DATA
NO_DATA
Error message
Job ${jobId} is offline or removed What it means
The job detail page loaded and the in-page script detected the site's expired/offline marker (data.error='EXPIRED'), so the library throws CliError('NO_DATA') indicating the job posting is offline or removed. The page exists but no longer serves job content.
Source
Thrown at clis/51job/detail.js:78
description: (() => {
const box = document.querySelector('.bmsg.job_msg') || document.querySelector('.job_msg');
if (!box) return '';
const clone = box.cloneNode(true);
clone.querySelectorAll('.fp, .mt10, script, style').forEach(n => n.remove());
return (clone.innerText || '').trim();
})(),
welfare: all('.t1 span, .jtag .t1 span'),
category: pick('职能类别'),
address: pick('上班地址'),
ageRequirement: pick('年龄要求'),
company: companyA?.innerText?.trim() || '',
companyUrl: companyA?.href || '',
companyTag: sel('.com_tag'),
};
})()`;
const data = await page.evaluate(script);
if (data.error === 'EXPIRED') {
throw new CliError('NO_DATA', `Job ${jobId} is offline or removed`);
}
if (!data.title) {
throw new CliError('NO_DATA', `Could not parse job detail for ${jobId}; page may have changed layout`);
}
// meta looks like "北京-丰台区 | 3年及以上 | 本科"
const [locRaw, workYear, degree] = (data.meta || '').split('|').map(s => s.trim());
// companyTag looks like "国企\n\n150-500人\n\n电子技术/半导体/集成电路"
const tagParts = (data.companyTag || '').split(/\n+/).map(s => s.trim()).filter(Boolean);
return [{
jobId,
title: data.title,
salary: data.salary || '',
location: locRaw || '',
workYear: workYear || '',
degree: degree || '',
category: data.category || '',View on GitHub (pinned to 49907e53dc)
Solutions
- Treat the job as gone: remove it from your dataset or mark it expired
- Re-fetch fresh ids from a current search/hot listing instead of cached ids
- If the job should exist, search for the title/company to find a reposted id
- Retry once in case of a transient geo/bot interstitial misread as expired
Example fix
// before
const detail = await cli.detail({ jobId: cachedJobId });
// after
try {
const detail = await cli.detail({ jobId: cachedJobId });
} catch (e) {
if (e.code === 'NO_DATA') return markExpired(cachedJobId);
throw e;
} Defensive patterns
Strategy: try-catch
Validate before calling
if (!/^\d{6,12}$/.test(String(jobId ?? ''))) throw new Error('bad jobId'); // only format is pre-checkable Type guard
const isLiveDetail = (d) => d != null && typeof d.title === 'string' && d.title.length > 0;
Try / catch
try {
return await cli.detail({ jobId });
} catch (e) {
if (e.code === 'NO_DATA') return markJobExpired(jobId); // expected for old listings
throw e;
} Prevention
- Treat this as expected when crawling aged listings — handle, don't crash
- Prefer ids freshly obtained from search/hot results
- Track expiry rates per crawl batch to detect stale sources
- Search for reposts under a new id when a job matters
When it happens
Trigger: Calling the detail subcommand with a jobId whose posting has been taken down, expired, filled, or delisted by the employer — the in-page script returns error='EXPIRED' and clis/51job/detail.js:78 throws.
Common situations: Crawling old listings from a cache or search index; jobs that closed between listing and detail fetch; region-locked postings that render as expired from abroad; employers reposting under a new id after closing the old one.
Related errors
- NO_DATA
- No coaches for ${fromCity} to ${toCity} on ${date}
- No trains for ${fromName} to ${toName} on ${date}
- ${command} returned no results
- EMPTY_RESULT
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/dbc8334904deb09d.
Report an issue: GitHub.