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

  1. Treat the job as gone: remove it from your dataset or mark it expired
  2. Re-fetch fresh ids from a current search/hot listing instead of cached ids
  3. If the job should exist, search for the title/company to find a reposted id
  4. 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

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


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/dbc8334904deb09d. Report an issue: GitHub.