{"record":{"id":"6056ba645eb62c41","repo":"santifer/career-ops","slug":"eightfold-cannot-derive-api-url-for-entry-name","errorCode":null,"errorMessage":"eightfold: cannot derive API URL for ${entry.name}","messagePattern":"eightfold: cannot derive API URL for (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"providers/eightfold.mjs","lineNumber":277,"sourceCode":"  return Number.isFinite(hint) && hint > 0 ? Math.min(fromEntry, Math.floor(hint)) : fromEntry;\n}\n\n/** @type {Provider} */\nexport default {\n  id: 'eightfold',\n\n  detect(entry) {\n    try {\n      const tenant = resolveTenant(entry);\n      return tenant ? { url: buildApiUrl(tenant, 0, PAGE_SIZE) } : null;\n    } catch {\n      return null;\n    }\n  },\n\n  async fetch(entry, ctx) {\n    const tenant = resolveTenant(entry);\n    if (!tenant) throw new Error(`eightfold: cannot derive API URL for ${entry.name}`);\n\n    const maxPages = resolveMaxPages(entry, ctx);\n    const all = [];\n    /** @type {number|null} */\n    let total = null;\n\n    for (let page = 0; page < maxPages; page++) {\n      const start = page * PAGE_SIZE;\n      const apiUrl = buildApiUrl(tenant, start, PAGE_SIZE);\n      assertEightfoldUrl(apiUrl); // SSRF guard before every fetch\n      if (page > 0) await sleep(INTER_PAGE_DELAY_MS, ctx);\n\n      const json = /** @type {any} */ (await fetchJsonWithRetry(\n        /** @type {any} */ (ctx),\n        apiUrl,\n        {\n          // redirect:'error' prevents SSRF via a server-side redirect; with\n          // assertEightfoldUrl above it guarantees the final hostname stays","sourceCodeStart":259,"sourceCodeEnd":295,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/eightfold.mjs#L259-L295","documentation":"provider.fetch() starts by resolving the tenant (host + optional domain) from the portal entry via resolveTenant(), which accepts only entry.api or entry.careers_url values that parse as HTTPS URLs on a *.eightfold.ai host. If neither field qualifies, fetch throws this error naming the entry — it cannot construct the /api/apply/v2/jobs endpoint, so it refuses to guess.","triggerScenarios":"fetch(entry) with entry.api missing/unset and entry.careers_url either absent, not a string, not parseable by new URL(), not https:, or on a host that fails EIGHTFOLD_HOST_RE (e.g. a branded CNAME like careers.company.com, an http: URL, or a malformed URL like 'bayer.eightfold.ai' with no scheme).","commonSituations":"A portals.yml entry points at the company's branded careers site instead of the canonical eightfold.ai tenant host; the careers_url lost its https:// scheme during hand-editing; the entry uses a custom field name (e.g. url: instead of careers_url:) that resolveTenant never reads; detect() returned null for this entry but an explicit provider selection forced fetch anyway.","solutions":["Set careers_url to https://<tenant>.eightfold.ai/careers (e.g. https://bayer.eightfold.ai/careers), or set entry.api directly to the eightfold.ai host — api takes precedence over careers_url.","Check the URL parses and uses https:// — a bare hostname like bayer.eightfold.ai is skipped silently by resolveTenant and surfaces as this error.","Confirm the hostname is a single-label subdomain of eightfold.ai (matches /^[a-z0-9-]+\\.eightfold\\.ai$/i); branded CNAMEs like careers.company.com are rejected by design.","If the entry's branded careers page must stay, keep it in careers_url and add an explicit api: https://<tenant>.eightfold.ai field to pin the tenant."],"exampleFix":"// before\n- name: Bayer\n  careers_url: https://careers.bayer.com\n// after\n- name: Bayer\n  careers_url: https://careers.bayer.com   # display only\n  api: https://bayer.eightfold.ai/careers  # tenant host pinned via api","handlingStrategy":"validation","validationCode":"function canResolveEightfoldTenant(entry) {\n  for (const raw of [entry?.api, entry?.careers_url]) {\n    if (typeof raw !== 'string' || !raw) continue;\n    try {\n      const u = new URL(raw);\n      if (u.protocol === 'https:' && /^[a-z0-9-]+\\.eightfold\\.ai$/i.test(u.hostname)) return true;\n    } catch { /* skip */ }\n  }\n  return false;\n}","typeGuard":"const hasEightfoldTenant = (entry) => canResolveEightfoldTenant(entry);","tryCatchPattern":"if (!canResolveEightfoldTenant(entry)) {\n  console.warn(`Skipping ${entry.name}: no https://*.eightfold.ai api/careers_url`);\n} else {\n  const jobs = await provider.fetch(entry, ctx);\n}","preventionTips":["Require https:// scheme in careers_url — bare hostnames are silently skipped by resolveTenant","Use entry.api to pin the eightfold.ai host when the careers page is branded","Run provider.detect(entry) first; a null result predicts this fetch error","Keep the tenant regex at hand when hand-editing portals.yml"],"tags":["configuration","url-validation","provider","eightfold"],"backgroundTag":"invalid-config-value","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}