{"record":{"id":"f28717a3bbcc230f","repo":"santifer/career-ops","slug":"oraclecloud-cannot-derive-api-url-for-entry-nam","errorCode":null,"errorMessage":"oraclecloud: cannot derive API URL for ${entry.name}","messagePattern":"oraclecloud: cannot derive API URL for (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/oraclecloud.mjs","lineNumber":233,"sourceCode":"  return out;\n}\n\n/** @type {Provider} */\nexport default {\n  id: 'oraclecloud',\n\n  detect(entry) {\n    try {\n      const site = resolveSite(entry);\n      return site ? { url: buildApiUrl(site, 0, PAGE_SIZE) } : null;\n    } catch {\n      return null;\n    }\n  },\n\n  async fetch(entry, ctx) {\n    const site = resolveSite(entry);\n    if (!site) throw new Error(`oraclecloud: cannot derive API URL for ${entry.name}`);\n\n    const maxPages = Number.isInteger(entry.max_pages) && entry.max_pages > 0\n      ? Math.min(entry.max_pages, MAX_PAGES)\n      : MAX_PAGES;\n\n    const all = [];\n    let total = null;\n    for (let page = 0; page < maxPages; page++) {\n      const offset = page * PAGE_SIZE;\n      const apiUrl = buildApiUrl(site, offset, PAGE_SIZE);\n      assertOracleUrl(apiUrl); // SSRF guard before every fetch\n      if (page > 0) await sleep(INTER_PAGE_DELAY_MS, ctx);\n\n      const json = await fetchJsonWithRetry(ctx, apiUrl, {\n        redirect: 'error',\n        headers: { 'User-Agent': BROWSER_LIKE_USER_AGENT, Accept: 'application/json' },\n      }, RETRY_POLICY);\n","sourceCodeStart":215,"sourceCodeEnd":251,"githubUrl":"https://github.com/santifer/career-ops/blob/e7abd431fce9348a95261acac9e0c14779c35df8/providers/oraclecloud.mjs#L215-L251","documentation":"The Oracle Recruiting provider could not derive an API base site from the entry configuration. fetch() calls resolveSite(entry), which extracts an allowlisted *.fa.*.oraclecloud*.com host from the entry (careers_url or explicit config); when it returns null the provider has nothing to call and throws instead of guessing. Unlike error 360 this is usually a missing/empty config value, not an untrusted one.","triggerScenarios":"Calling providers/oraclecloud.mjs fetch(entry, ctx) when entry has no careers_url, careers_url is not a string, the URL is malformed, or resolveSite's regex loop matches no allowlisted hostname — e.g. an oraclecloud entry whose URL points to a Greenhouse/Ashby domain.","commonSituations":"A portals.yml entry tagged with the wrong provider id (company actually uses another ATS); an entry added with only a name and no careers_url; or careers_url pointing at a redirect vanity domain that no provider claims.","solutions":["Add or correct careers_url in the portals.yml entry so it points to the tenant's *.fa.[region.][ocs.]oraclecloud[1-99].com host.","Check that the entry's provider id is actually oraclecloud and not a mis-assigned provider for a different ATS.","If the board is only reachable via a vanity domain, set the explicit Oracle tenant host in the entry config that resolveSite reads.","Use audit-portals.mjs on the entry to confirm which provider actually claims that careers_url before keeping it under oraclecloud."],"exampleFix":"// before (portals.yml)\n- name: Acme\n  provider: oraclecloud\n// after\n- name: Acme\n  provider: oraclecloud\n  careers_url: https://acme.fa.ocs.oraclecloud.com/hcmUI/CandidateExperience/en/sites/CX_1","handlingStrategy":"validation","validationCode":"function hasDerivableOracleSite(entry) {\n  const raw = typeof entry.careers_url === 'string' ? entry.careers_url : '';\n  if (!raw) return false;\n  try {\n    const { hostname } = new URL(raw);\n    return /^[a-z0-9-]+\\.fa\\.(?:[a-z0-9-]+\\.)?(?:ocs\\.)?oraclecloud(?:[1-9][0-9]?)?\\.com$/i.test(hostname);\n  } catch { return false; }\n}\nif (!hasDerivableOracleSite(entry)) throw new Error(`oraclecloud: entry ${entry.name} needs an allowlisted careers_url`);","typeGuard":"function hasOracleCareersUrl(entry) {\n  return entry != null && typeof entry === 'object' &&\n    typeof entry.careers_url === 'string' && entry.careers_url.length > 0 &&\n    (() => { try { return isOracleUrl(entry.careers_url); } catch { return false; } })();\n}","tryCatchPattern":"try {\n  await oracleProvider.fetch(entry, ctx);\n} catch (e) {\n  if (String(e.message).startsWith('oraclecloud: cannot derive API URL')) {\n    logger.warn({ entry: entry.name }, 'no usable Oracle host in entry config — skipping');\n    return null;\n  }\n  throw e;\n}","preventionTips":["Make careers_url a required field for oraclecloud entries; validate portals.yml with a schema (e.g. zod or a CI script) before runs.","Check provider assignment with audit-portals.mjs — wrong ATS id is the usual cause.","Grep for misspelled keys (career_url, careersUrl) in portals.yml periodically.","Keep tenant host and vanity domain documented in the entry comments so future edits don't drop the field."],"tags":["config","missing-config","oracle","url-derivation"],"backgroundTag":"missing-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"}