{"record":{"id":"665b366c86594726","repo":"santifer/career-ops","slug":"rheinmetall-cannot-resolve-vacancies-url-for-en","errorCode":null,"errorMessage":"rheinmetall: cannot resolve vacancies URL for ${entry.name}","messagePattern":"rheinmetall: cannot resolve vacancies URL for (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/rheinmetall.mjs","lineNumber":102,"sourceCode":"function resolveMaxPages(entry) {\n  const v = entry?.max_pages;\n  if (Number.isInteger(v) && v > 0) return Math.min(v, MAX_PAGES);\n  return MAX_PAGES;\n}\n\n/** @type {Provider} */\nexport default {\n  id: 'rheinmetall',\n\n  detect(entry) {\n    const url = entry.api || entry.careers_url || '';\n    if (typeof url !== 'string') return null;\n    return resolveListUrl({ api: url }) ? { url } : null;\n  },\n\n  async fetch(entry, ctx) {\n    const listUrl = resolveListUrl(entry);\n    if (!listUrl) throw new Error(`rheinmetall: cannot resolve vacancies URL for ${entry.name}`);\n    const origin = new URL(listUrl).origin;\n\n    const wait = (ms) => (ctx.sleep ? ctx.sleep(ms) : new Promise((r) => setTimeout(r, ms)));\n    const maxPages = resolveMaxPages(entry);\n    const jobs = [];\n    const seen = new Set();\n\n    for (let page = 1; page <= maxPages; page++) {\n      if (page > 1) await wait(PAGE_DELAY_MS);\n      const html = await ctx.fetchText(`${listUrl}?page=${page}`, {\n        headers: { accept: 'text/html' },\n      });\n      const rows = parseVacancies(html, origin);\n      if (rows.length === 0) {\n        if (page === 1) console.warn(`rheinmetall: page 1 returned no vacancy cards for ${entry.name} — markup may have changed`);\n        break; // past the last page\n      }\n","sourceCodeStart":84,"sourceCodeEnd":120,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/rheinmetall.mjs#L84-L120","documentation":"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.","triggerScenarios":"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().","commonSituations":"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.","solutions":["Ensure entry.api or entry.careers_url is a valid https://*.rheinmetall.com URL (e.g. https://www.rheinmetall.com/en/career/vacancies).","Confirm the host is rheinmetall.com or a direct subdomain — third-party hosted boards will not resolve.","Call detect(entry) first; it returns null when resolveListUrl cannot produce a URL.","Check the config for a typo'd hostname (rheinmetal.com without the double-l)."],"exampleFix":"# before\n- name: Rheinmetall\n  provider: rheinmetall\n# after\n- name: Rheinmetall\n  provider: rheinmetall\n  api: https://www.rheinmetall.com/en/career/vacancies","handlingStrategy":"validation","validationCode":"function isRheinmetallUrl(raw) {\n  try {\n    const u = new URL(raw);\n    const h = u.host.toLowerCase();\n    return h === 'rheinmetall.com' || h.endsWith('.rheinmetall.com');\n  } catch { return false; }\n}\nif (!isRheinmetallUrl(entry.api || entry.careers_url || '')) {\n  console.warn(`skip ${entry.name}: not a *.rheinmetall.com URL`);\n}","typeGuard":"null","tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (e) {\n  if (/cannot resolve vacancies URL/.test(e.message)) {\n    console.warn(`[skip] ${entry.name}: needs a rheinmetall.com api/careers_url`);\n  } else throw e;\n}","preventionTips":["Set entry.api or entry.careers_url to a valid https://*.rheinmetall.com URL.","Call detect(entry) before fetch; it returns null when the host check fails.","Subsidiaries on non-rheinmetall domains need a different provider."],"tags":["config","provider","rheinmetall","missing-url","resolution","hostname-allowlist"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}