santifer/career-ops · error · Error

rheinmetall: cannot resolve vacancies URL for ${entry.name}

Error message

rheinmetall: cannot resolve vacancies URL for ${entry.name}

What it means

fetch() throws when resolveListUrl(entry) returns null. resolveListUrl reads entry.api || entry.careers_url, parses it, and requires the host to be exactly 'rheinmetall.com' or end with '.rheinmetall.com'; it then normalizes the path to /career/vacancies or /en/career/vacancies. A non-rheinmetall host, an unparseable URL, or a missing URL field all return null.

Source

Thrown at providers/rheinmetall.mjs:102

function resolveMaxPages(entry) {
  const v = entry?.max_pages;
  if (Number.isInteger(v) && v > 0) return Math.min(v, MAX_PAGES);
  return MAX_PAGES;
}

/** @type {Provider} */
export default {
  id: 'rheinmetall',

  detect(entry) {
    const url = entry.api || entry.careers_url || '';
    if (typeof url !== 'string') return null;
    return resolveListUrl({ api: url }) ? { url } : null;
  },

  async fetch(entry, ctx) {
    const listUrl = resolveListUrl(entry);
    if (!listUrl) throw new Error(`rheinmetall: cannot resolve vacancies URL for ${entry.name}`);
    const origin = new URL(listUrl).origin;

    const wait = (ms) => (ctx.sleep ? ctx.sleep(ms) : new Promise((r) => setTimeout(r, ms)));
    const maxPages = resolveMaxPages(entry);
    const jobs = [];
    const seen = new Set();

    for (let page = 1; page <= maxPages; page++) {
      if (page > 1) await wait(PAGE_DELAY_MS);
      const html = await ctx.fetchText(`${listUrl}?page=${page}`, {
        headers: { accept: 'text/html' },
      });
      const rows = parseVacancies(html, origin);
      if (rows.length === 0) {
        if (page === 1) console.warn(`rheinmetall: page 1 returned no vacancy cards for ${entry.name} — markup may have changed`);
        break; // past the last page
      }

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Ensure entry.api or entry.careers_url is a valid https://*.rheinmetall.com URL (e.g. https://www.rheinmetall.com/en/career/vacancies).
  2. Confirm the host is rheinmetall.com or a direct subdomain — third-party hosted boards will not resolve.
  3. Call detect(entry) first; it returns null when resolveListUrl cannot produce a URL.
  4. Check the config for a typo'd hostname (rheinmetal.com without the double-l).

Example fix

# before
- name: Rheinmetall
  provider: rheinmetall
# after
- name: Rheinmetall
  provider: rheinmetall
  api: https://www.rheinmetall.com/en/career/vacancies
Defensive patterns

Strategy: validation

Validate before calling

function isRheinmetallUrl(raw) {
  try {
    const u = new URL(raw);
    const h = u.host.toLowerCase();
    return h === 'rheinmetall.com' || h.endsWith('.rheinmetall.com');
  } catch { return false; }
}
if (!isRheinmetallUrl(entry.api || entry.careers_url || '')) {
  console.warn(`skip ${entry.name}: not a *.rheinmetall.com URL`);
}

Type guard

null

Try / catch

try {
  await provider.fetch(entry, ctx);
} catch (e) {
  if (/cannot resolve vacancies URL/.test(e.message)) {
    console.warn(`[skip] ${entry.name}: needs a rheinmetall.com api/careers_url`);
  } else throw e;
}

Prevention

When it happens

Trigger: entry.api and entry.careers_url are both missing/empty; the URL host is not rheinmetall.com or a subdomain (e.g. a third-party ATS URL); the URL string is malformed and throws inside new URL().

Common situations: A portals.yml row for a Rheinmetall subsidiary points at a non-rheinmetall.com careers domain; the entry was misclassified; the URL field was dropped during data migration.

Related errors


AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13). Data as JSON: /api/errors/665b366c86594726. Report an issue: GitHub.