{"record":{"id":"7561a8ca1efb0bfc","repo":"santifer/career-ops","slug":"deutschebahn-cannot-resolve-db-jobs-search-id-for","errorCode":null,"errorMessage":"deutschebahn: cannot resolve db.jobs search id for ${entry.name}","messagePattern":"deutschebahn: cannot resolve db\\.jobs search id for (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"providers/deutschebahn.mjs","lineNumber":113,"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: 'deutschebahn',\n\n  detect(entry) {\n    const url = entry.api || entry.careers_url || '';\n    if (typeof url !== 'string') return null;\n    return resolveConfig({ api: url }) ? { url } : null;\n  },\n\n  async fetch(entry, ctx) {\n    const cfg = resolveConfig(entry);\n    if (!cfg) throw new Error(`deutschebahn: cannot resolve db.jobs search id for ${entry.name}`);\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    // Each page is a ~450KB HTML fragment; on a walk of up to 60 pages, an\n    // occasional single-page timeout/abort is a transient blip, not a board\n    // failure — fetchTextWithRetry absorbs it instead of failing the whole scan.\n\n    for (let page = 0; page < maxPages; page++) {\n      if (page > 0) await wait(PAGE_DELAY_MS);\n      const url = `${cfg.searchBase}?qli=true&query=&sort=score&itemsPerPage=${ITEMS_PER_PAGE}&pageNum=${page}`;\n      const html = await fetchTextWithRetry(ctx, url, { headers: { accept: 'text/html' }, redirect: 'error' });\n      const rows = parseHits(html, cfg.origin);\n      if (rows.length === 0) break; // past the last page\n\n      let fresh = 0;","sourceCodeStart":95,"sourceCodeEnd":131,"githubUrl":"https://github.com/santifer/career-ops/blob/e7abd431fce9348a95261acac9e0c14779c35df8/providers/deutschebahn.mjs#L95-L131","documentation":"The Deutsche Bahn provider scrapes DB's db.jobs portal, which requires a numeric search-config id in the URL path (/service/search/de-de/{searchId}). resolveConfig() must derive that id from the entry's `api:`/`careers_url`; when it returns null — because the value is missing, unparseable, or its host is not db.jobs (or a *.db.jobs subdomain) — fetch() throws this error. Note the id itself has a well-known fallback ('5441588'), so a null return means the HOST check failed, not that the id is absent.","triggerScenarios":"fetch(entry) is called with an entry whose api/careers_url is (a) empty or not a valid URL, (b) not on host db.jobs / *.db.jobs (e.g. the branded jobs.deutschebahngroup.careers front, which only 302-redirects into db.jobs), or (c) an http/https URL on a foreign host — resolveConfig returns null in all cases and the error is thrown with the entry's name interpolated.","commonSituations":"Portals.yml configured with the branded careers URL jobs.deutschebahngroup.careers instead of a db.jobs URL; a typo in the hostname; an entry with no api/careers_url at all; DB migrating portals so the old host no longer matches the db.jobs check.","solutions":["Set the entry's `api:` to a db.jobs URL, e.g. https://db.jobs/service/search/de-de/5441588 — the search id is optional (well-known fallback) but the host must be db.jobs.","If you only have the branded URL, follow its redirect once manually and copy the resulting db.jobs URL into the config.","Check for hostname typos: the check is host === 'db.jobs' or host.endsWith('.db.jobs'), case-insensitive.","If DB has moved off db.jobs, update the provider's host check and URL patterns to the new portal."],"exampleFix":"// before (portals.yml)\n- name: deutsche-bahn\n  careers_url: https://jobs.deutschebahngroup.careers/search\n// after\n- name: deutsche-bahn\n  careers_url: https://jobs.deutschebahngroup.careers/search\n  api: https://db.jobs/service/search/de-de/5441588","handlingStrategy":"validation","validationCode":"function isDbJobsEntry(entry) {\n  const raw = entry.api || entry.careers_url || '';\n  try {\n    const u = new URL(raw);\n    const host = u.host.toLowerCase();\n    return u.protocol === 'https:' && (host === 'db.jobs' || host.endsWith('.db.jobs'));\n  } catch { return false; }\n}\nif (!isDbJobsEntry(entry)) console.warn(`${entry.name}: use a db.jobs api: URL, not the branded careers front`);","typeGuard":null,"tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (e) {\n  if (e.message.includes('cannot resolve db.jobs search id')) {\n    // config problem, not transient — fix the entry's api: host, don't retry\n    console.error(`Fix ${entry.name}: api: must be a db.jobs URL (e.g. https://db.jobs/service/search/de-de/5441588)`);\n  } else throw e;\n}","preventionTips":["Never use jobs.deutschebahngroup.careers as the api: value — it only redirects into db.jobs.","Pin the well-known search id (5441588) explicitly in the URL to survive fallback changes.","Test resolveConfig on new entries with a quick node -e snippet before committing them.","Watch for DB portal migrations and update host checks if the board moves."],"tags":["config","url","provider"],"backgroundTag":"invalid-config-value","analyzedSha":"e7abd431fce9348a95261acac9e0c14779c35df8","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}